diff --git a/.cursorrules b/.cursorrules
new file mode 100644
index 00000000..06f3caf4
--- /dev/null
+++ b/.cursorrules
@@ -0,0 +1,102 @@
+# zkxw 工程 Cursor 规则
+
+本仓库含 Web 管理端(`web-admin`)、小程序端(`miniprogram`)等子工程。实现或修改 UI 时,**以文档为权威来源**,本文件仅作快速索引与强约束摘要。
+
+## 权威文档(必读路径)
+
+- **Web 管理端(Fresh Mint)**:`docs/web-admin-ui-spec-fresh-mint.md`
+ 适用范围:`web-admin`(Vue 3 + Element Plus + SCSS/Tailwind)。色值、布局、列表、弹窗、树形路径等均以该文档及其中引用的 `fresh-mint-green.html` 为准。
+- **小程序端(三墙 Demo 体系)**:`docs/miniprogram-ui-spec-sanqiang.md`
+ 适用范围:`miniprogram`(客户端 / 供应商端 / 配送员端)。视觉以该文档及 `mobile-ui/sanqiang-*.html` 为准。
+
+修改对应子工程前,若涉及界面,应先检索上述文档中的章节(类名、Token、结构顺序)。
+
+---
+
+## Web 管理端(`web-admin`)摘要
+
+### 技术栈与实现方式
+
+- Vue 3 + Element Plus;主题与皮肤通过 **CSS 变量 + Element 主题变量 + 全局覆盖** 对齐规范,**勿随意改 Token 色值**。
+- 树形主数据全路径:`web-admin/src/utils/tree-path.ts`(如 `buildTreePathLabelFromFlat`、`buildPathLabelMapFromTreeRoots`、`resolveTreePathDisplay`)。路径分隔符统一为 **` / `**(半角斜杠两侧各一空格)。
+- 参考实现:商品查看弹窗 `web-admin/src/components/business/product-view-dialog/index.vue`;列表与分页对齐 `#/base/unit` 等已改造页。
+
+### 视觉与 Token(与规范一致)
+
+- 主色链:`--primary` `#10b981`、`--primary-dark` `#059669`、`--primary-bg` `#ecfdf5`;中性字与边框见规范第 2 节表格。
+- 风格:清爽 B 端、薄荷绿 + Slate;**避免一屏多种高饱和色**;次要信息用 muted 与 `.tag.muted`。
+- 字体:Inter + 系统栈;正文 14px / line-height 1.5。
+
+### 布局与页面骨架
+
+- 侧栏 220px、顶栏 56px、内容区 padding 24px;面板圆角 10px,输入/按钮等 8px 级圆角见规范。
+- 列表页典型顺序:**筛选 `filter-block`(可选)→ 列表工具栏 → `list-body` / `list-wrap`(含 `el-table`)→ 分页**。
+- **单标题**:列表面板仅一个 `panel-top` 主标题;筛选区内不要再加「筛选条件」等重复标题。
+- **单分隔线**:工具栏与列表之间靠 `list-body` 顶边框;列表与分页之间保留列表底边与分页顶边规则,避免双线叠加(见规范列表与分页章节)。
+
+### 筛选与表单
+
+- 筛选栅格、label 11px、控件高度 36px、placeholder 规则等见规范第 5 节。
+- 筛选区内部:**不要用横向分割线** 区分多行条件与展开区,仅用 gap / margin。
+- 弹窗表单:**`label-position="top"`**;两列可用 `form-grid-2`;长字段 `grid-column: 1 / -1`。
+- 弹窗底栏:取消/确定建议 **Close / Check** 图标,图标与文字间距 **6px**;footer 按钮高度与圆角等见规范 `el-dialog` 小节。
+- 长表单:`el-form` 建议 **`scroll-to-error`** 与 **`scroll-into-view-options`**;校验失败可配合 `ElMessageBox.alert` 提示(与已有页一致)。
+- 遮罩:**无背景模糊**(`backdrop-filter: none`、`modal-class` 等与现有 `order-module-dialog-mask` 等策略一致)。
+
+### 列表工具栏与表格
+
+- 工具栏:**「已选 N 项」在最前**,其后从左到右排列主按钮与批量操作;**禁止**用 `space-between` 把整组按钮推到行末。
+- **同一行仅第一个主行动** 使用主色实心(`type="primary"` / `btn-main`);批量删除、导入导出等为中性描边。
+- **操作列**:`fixed="right"`,预留足够宽度(建议 min-width ≥ 260px),避免按钮被裁切。
+- 主表:`list-table` + `stripe`;与已对齐订单列表一致时 **勿随意加 `border`**(以规范及参考页为准)。
+- 分页:表格与 **`el-pagination`** 同放在 **`list-body.list-wrap`** 内;外层 **`.pagination.list-pager-bar`**,`margin-top: 0`,上边框与 padding 见规范第 7.9 节。
+
+### 树形选择器与查看态
+
+- 下拉树节点文案:**本级名称**;选中后输入框、表格列、导出、查看弹窗:**全路径**(见规范树形与第 5.6 节)。
+
+### 状态与业务色
+
+- 进行中/主状态:浅绿底 + 深绿字;警告琥珀 `#d97706` 系;危险红系用于强提醒;与订单/配送枚举映射时保持语义一致(见规范第 12 节)。
+
+---
+
+## 小程序端(`miniprogram`)摘要
+
+### Token 与适配
+
+- 品牌绿:`#0a9f6d` / `#087f57` / `#e9f9f2`;大麦黄辅助:`#d6a343` / `#fff7e8`;中性背景与边框见规范第 2.1 节。
+- 主基线宽度 **375px**,兼容 **414px**;间距节奏 8–16;卡片圆角 12–16px,大容器 18–22px。
+
+### 三端统一骨架
+
+- 顺序一般为:Header(绿渐变)→ 统计/提示(可选)→ 功能宫格 → 主列表/任务 → 底部固定区(客户端:结算条 + TabBar)。
+- Header:绿色渐变、标题左对齐、操作右对齐、下缘大圆角(约 20–24px)。
+
+### 组件与媒体(强约束)
+
+- 列表缩略图:**1:1**,`aspectFill`;轮播:**4:3**,`aspectFill`;详情图:**widthFix**;视频建议 720p 内、尽量短(见规范第 7.0 节)。
+- 状态标签语义:`warning` 琥珀待处理、`success` 绿色进行中或完成、`danger` 红色异常;避免同屏过多高饱和色。
+- 底部结算条:数量 → 总价 → 主按钮;主按钮视觉优先级最高。
+- 触达区域不小于 **44×44**;注意横滑与纵向滚动冲突(尤其一级分类横滑)。
+
+### 从 Demo 到页面
+
+- 规范第 11 节给出 Demo class 到 `pages/**/*.vue` 的建议类名;优先统一 Token 与骨架,再收敛公共样式到 `src/styles` 或公共组件。
+- 新增页面类型应先在 `docs/miniprogram-ui-spec-sanqiang.md` 补充结构说明再落代码(见规范第 10 节)。
+
+---
+
+## 通用开发原则
+
+- **最小必要改动**:不扩大范围、不做无关重构;新增代码与周边文件的命名、类型、导入风格保持一致。
+- **跨端颜色不要混用**:管理端用 Fresh Mint 链(`#10b981` 等);小程序用三墙 Token(`#0a9f6d` 等);除非产品明确要求对齐,否则不要简单复制另一端的色值。
+- **后端/API 变更**:若 UI 改动依赖接口字段(如树节点 id、分类全路径),需同步评估接口与类型定义,避免前端仅末级展示造成歧义。
+- 提交信息使用完整句子,说明「改了什么、为何」,避免无意义前缀堆砌。
+
+---
+
+## 语言
+
+- 与用户沟通时:若用户使用简体中文,助手回复使用**简体中文**。
+- 代码注释与用户可见文案:遵循项目现有语言习惯(中文业务文案以产品为准)。
diff --git a/backend/.env.example b/backend/.env.example
new file mode 100644
index 00000000..2b583e14
--- /dev/null
+++ b/backend/.env.example
@@ -0,0 +1,45 @@
+# 数据库连接配置示例
+# DATABASE_URL 格式:mysql+pymysql://user:password@host:port/dbname
+DATABASE_URL=mysql+pymysql://root:password@localhost:3306/zkxw
+
+# JWT配置示例
+# SECRET_KEY=your-secret-key-change-in-production
+# ALGORITHM=HS256
+# ACCESS_TOKEN_EXPIRE_MINUTES=30
+
+# 应用配置示例
+# APP_NAME=中科兴皖一体化门户
+# DEBUG=true
+
+# 登录 RSA(Web/小程序加密登录;多实例须配置同一私钥 PEM)
+# LOGIN_RSA_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
+# 仅本地用 Swagger/Apifox 明文测登录时可临时打开(生产务必 false)
+# LOGIN_ALLOW_PLAINTEXT_LOGIN=false
+# LOGIN_ENCRYPT_TS_SKEW_SEC=300
+
+# Redis 配置(用于会话/Token 吊销;需在后端运行时配置)
+# REDIS_HOST=127.0.0.1
+# REDIS_PORT=6379
+# REDIS_DB=0
+# REDIS_PASSWORD=
+# REDIS_TIMEOUT_SEC=30
+# REDIS_SSL_ENABLED=false
+
+# MinIO 配置示例
+# STORAGE_TYPE=minio
+# MINIO_ENDPOINT=127.0.0.1:9000
+# MINIO_ACCESS_KEY=CHANGEME
+# MINIO_SECRET_KEY=CHANGEME
+# MINIO_BUCKET=sanqiang
+# MINIO_SECURE=false
+# MINIO_BASE_URL=https://www.chengyantek.cn/sanqiang
+
+# 微信支付配置示例
+# 小程序支付使用 JSAPI(trade_type=JSAPI)
+# WXPAY_MCHID=1900000109
+# WXPAY_MINI_APPID=wx1234567890abcdef
+# WXPAY_SECRET=your-miniapp-secret
+# WXPAY_API_KEY=your-v2-api-key
+# WXPAY_NOTIFY_URL=https://www.chengyantek.cn/api/payment/notify
+# WXPAY_REFUND_NOTIFY_URL=https://www.chengyantek.cn/api/payment/refund-notify
+# WXPAY_SANDBOX=false
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 00000000..d55e35b1
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,90 @@
+# 中科兴皖一体化门户后端(空壳)
+
+当前 `app.main` **不连接数据库、不注册任何业务路由**,仅提供根路径与 `/health`,便于占位或 CI。原 `app/api` 下 Python 文件已删除(请从 git 历史恢复)。`app/models`、`app/crud` 等目录仍保留为未引用代码,可按需继续清理。
+
+## 技术栈
+
+- FastAPI - 现代高性能异步 Web 框架
+- SQLAlchemy - ORM 数据库映射
+- Pydantic - 数据验证
+- JWT - 用户认证
+- MySQL/PostgreSQL - 数据库
+
+## 项目结构
+
+```
+backend/
+├── app/
+│ ├── api/ # 包占位(路由实现已删除)
+│ ├── core/ # 核心配置
+│ ├── models/ # 数据库模型
+│ ├── schemas/ # Pydantic schemas
+│ ├── services/ # 业务逻辑
+│ └── main.py # 应用入口
+├── requirements.txt
+└── README.md
+```
+
+## 快速开始
+
+### 1. 创建虚拟环境
+
+```bash
+python -m venv venv
+venv\Scripts\activate # Windows
+# source venv/bin/activate # Linux/Mac
+```
+
+### 2. 安装依赖
+
+```bash
+pip install -r requirements.txt
+```
+
+### 3. 配置环境变量
+
+复制 `.env.example` 为 `.env` 并修改配置:
+
+```env
+# 数据库配置
+DATABASE_URL=mysql+pymysql://root:password@localhost:3306/zkxw
+
+# JWT配置
+SECRET_KEY=your-secret-key-change-in-production
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=30
+
+# 应用配置
+APP_NAME=中科兴皖一体化门户
+DEBUG=true
+```
+
+### 4. 启动服务
+
+```bash
+uvicorn app.main:app --reload --port 8000
+uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
+
+```
+
+服务启动后,访问:
+- API 文档:http://127.0.0.1:8000/docs
+- ReDoc 文档:http://127.0.0.1:8000/redoc
+
+## API 路由分组
+
+| 前缀 | 说明 | 认证方式 |
+|------|------|----------|
+| `/api/admin` | 运营端接口 | JWT Token |
+| `/api/supplier` | 供应商端接口 | JWT Token |
+| `/api/client` | 客户端接口 | JWT Token |
+| `/api/deliver` | 配送员端接口 | JWT Token |
+| `/api/public` | 公共接口(登录等) | 无需认证 |
+
+## 开发规范
+
+- 使用 Pydantic 进行数据验证
+- 使用 SQLAlchemy ORM 进行数据库操作
+- 所有接口遵循 RESTful 规范
+- 密码使用 bcrypt 加密存储
+- 使用 JWT Token 进行用户认证
diff --git a/backend/app.pid b/backend/app.pid
new file mode 100644
index 00000000..4f4cd1ff
--- /dev/null
+++ b/backend/app.pid
@@ -0,0 +1 @@
+11632
diff --git a/backend/app/__init__.py b/backend/app/__init__.py
new file mode 100644
index 00000000..1f9cc0f0
--- /dev/null
+++ b/backend/app/__init__.py
@@ -0,0 +1 @@
+# 中科兴皖一体化门户后端(骨架)
diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py
new file mode 100644
index 00000000..c0127a03
--- /dev/null
+++ b/backend/app/api/__init__.py
@@ -0,0 +1,3 @@
+"""API 包:业务路由模块已删除,仅保留包占位。恢复历史实现请使用 git。"""
+
+__all__: list[str] = []
diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py
new file mode 100644
index 00000000..97483139
--- /dev/null
+++ b/backend/app/core/__init__.py
@@ -0,0 +1,3 @@
+from app.core.config import settings
+
+__all__ = ["settings"]
diff --git a/backend/app/core/admin_request_json.py b/backend/app/core/admin_request_json.py
new file mode 100644
index 00000000..deff0715
--- /dev/null
+++ b/backend/app/core/admin_request_json.py
@@ -0,0 +1,78 @@
+"""运营 Web(/web/admin)JSON 请求体解析。
+
+与 ApiCryptoMiddleware 配合:中间件未改写 body 时,在路由内解密 `{ enc, ts, nonce, edata }`
+并剥 `{ data: ... }`,避免 FastAPI 声明式 Body 在校验阶段只看到密文信封而 422。
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import Any, Optional, Tuple
+
+from fastapi import Request
+
+from app.core.request_payload import resolve_request_payload
+
+_log = logging.getLogger("sqxp")
+
+
+async def parse_admin_request_json(
+ request: Request,
+ current_user: dict,
+ *,
+ source: str,
+) -> Tuple[Any, Optional[str]]:
+ """
+ 读取 JSON body;dict 时走 resolve_request_payload(明文 / 密文 / data 包装)。
+
+ Returns:
+ (payload, None) 成功;
+ (_, err_msg) 密文解密失败(此时 payload 为原始 raw,调用方可忽略)。
+ """
+ dw = source.startswith("delivery_warning")
+ try:
+ raw: Any = await request.json()
+ except Exception as ex:
+ if dw:
+ _log.warning("parse_admin_request_json source=%s request.json() failed: %s", source, ex)
+ raw = {}
+ if isinstance(raw, dict):
+ if dw:
+ ed = raw.get("edata")
+ _log.info(
+ "parse_admin_request_json source=%s raw_keys=%s enc=%s edata_len=%s user_id=%s",
+ source,
+ sorted(raw.keys()),
+ int(raw.get("enc", 0) or 0),
+ len(ed) if isinstance(ed, str) else (len(str(ed)) if ed is not None else 0),
+ current_user.get("user_id"),
+ )
+ resolved = resolve_request_payload(raw, current_user, source=source)
+ if dw:
+ if isinstance(resolved, dict):
+ _log.info(
+ "parse_admin_request_json source=%s resolved_keys=%s",
+ source,
+ sorted(resolved.keys()),
+ )
+ else:
+ _log.info(
+ "parse_admin_request_json source=%s resolved_type=%s",
+ source,
+ type(resolved).__name__,
+ )
+ if resolved == {} and int(raw.get("enc", 0) or 0) == 1:
+ if dw:
+ _log.warning(
+ "parse_admin_request_json source=%s treating as decrypt failure (resolved empty, enc=1)",
+ source,
+ )
+ return raw, "密文解密失败"
+ return resolved, None
+ if dw:
+ _log.warning(
+ "parse_admin_request_json source=%s raw_not_dict type=%s",
+ source,
+ type(raw).__name__,
+ )
+ return raw, None
diff --git a/backend/app/core/api_crypto.py b/backend/app/core/api_crypto.py
new file mode 100644
index 00000000..92fae4af
--- /dev/null
+++ b/backend/app/core/api_crypto.py
@@ -0,0 +1,65 @@
+"""Web API 请求/响应加解密工具。"""
+from __future__ import annotations
+
+import base64
+import hashlib
+import json
+import os
+import time
+from typing import Any, Dict
+
+from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+
+
+class ApiCryptoError(Exception):
+ """API 加解密错误。"""
+
+
+def derive_key(fixed_secret: str, user_id: str, ts: int, nonce: str) -> bytes:
+ seed = f"{fixed_secret}:{user_id}:{ts}:{nonce}"
+ return hashlib.sha256(seed.encode("utf-8")).digest()
+
+
+def _b64url_encode(data: bytes) -> str:
+ return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
+
+
+def _b64url_decode(data: str) -> bytes:
+ pad = "=" * (-len(data) % 4)
+ return base64.urlsafe_b64decode((data + pad).encode("utf-8"))
+
+
+def encrypt_json(payload: Dict[str, Any], key: bytes) -> str:
+ iv = os.urandom(12)
+ aes = AESGCM(key)
+ raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
+ cipher = aes.encrypt(iv, raw, None)
+ return _b64url_encode(iv + cipher)
+
+
+def decrypt_json(cipher_text: str, key: bytes) -> Dict[str, Any]:
+ try:
+ data = _b64url_decode(cipher_text)
+ if len(data) < 13:
+ raise ApiCryptoError("密文长度非法")
+ iv = data[:12]
+ cipher = data[12:]
+ aes = AESGCM(key)
+ plain = aes.decrypt(iv, cipher, None)
+ obj = json.loads(plain.decode("utf-8"))
+ if not isinstance(obj, dict):
+ raise ApiCryptoError("解密后报文格式非法")
+ return obj
+ except ApiCryptoError:
+ raise
+ except Exception as exc:
+ raise ApiCryptoError("密文解析失败") from exc
+
+
+def gen_nonce_hex() -> str:
+ return os.urandom(12).hex()
+
+
+def is_ts_in_window(ts: int, skew_sec: int) -> bool:
+ now_ms = int(time.time() * 1000)
+ return abs(now_ms - int(ts)) <= max(30, int(skew_sec)) * 1000
diff --git a/backend/app/core/api_crypto_middleware.py b/backend/app/core/api_crypto_middleware.py
new file mode 100644
index 00000000..7ae3cb0d
--- /dev/null
+++ b/backend/app/core/api_crypto_middleware.py
@@ -0,0 +1,239 @@
+"""Web API 加解密中间件。"""
+from __future__ import annotations
+
+import asyncio
+import json
+import logging
+import time
+from typing import Any, Optional, Tuple
+from urllib.parse import urlencode
+
+import redis.asyncio as redis
+from jose import jwt
+from redis import exceptions as redis_exceptions
+from starlette.middleware.base import BaseHTTPMiddleware
+from starlette.requests import Request
+from starlette.responses import Response
+
+from app.core import api_crypto
+from app.core.config import settings
+from app.core.response import error_response
+
+logger = logging.getLogger("sqxp")
+
+_redis_client: Optional[redis.Redis] = None
+_local_nonce_cache: dict[str, float] = {}
+
+
+def _nonce_key(user_id: str, nonce: str) -> str:
+ return f"api-enc:nonce:{user_id}:{nonce}"
+
+
+def _should_skip_path(path: str) -> bool:
+ excludes = [x.strip() for x in (settings.API_ENCRYPT_EXCLUDE_PATHS or "").split(",") if x.strip()]
+ return path in excludes
+
+
+def _is_crypto_api(path: str) -> bool:
+ """仅对运营端与小程序业务 API 应用统一密文协议。"""
+ prefix = settings.API_V1_PREFIX
+ return path.startswith(f"{prefix}/web/") or path.startswith(f"{prefix}/mini/")
+
+
+def _claims_from_auth(auth_header: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
+ if not auth_header or not auth_header.startswith("Bearer "):
+ return None, None
+ token = auth_header[7:].strip()
+ if not token:
+ return None, None
+ try:
+ payload = jwt.decode(
+ token,
+ settings.SECRET_KEY,
+ algorithms=[settings.ALGORITHM],
+ options={"verify_signature": False, "verify_exp": False},
+ )
+ return payload.get("sub"), token
+ except Exception:
+ return None, token
+
+
+def _get_redis_client() -> Optional[redis.Redis]:
+ global _redis_client
+ if not settings.REDIS_HOST:
+ return None
+ if _redis_client is None:
+ _redis_client = redis.Redis(
+ host=settings.REDIS_HOST,
+ port=settings.REDIS_PORT,
+ db=settings.REDIS_DB,
+ password=settings.REDIS_PASSWORD or None,
+ socket_timeout=settings.REDIS_TIMEOUT_SEC,
+ ssl=settings.REDIS_SSL_ENABLED,
+ decode_responses=True,
+ )
+ return _redis_client
+
+
+async def _mark_nonce_once(user_id: str, nonce: str) -> bool:
+ ttl_sec = max(30, int(settings.API_ENCRYPT_NONCE_TTL_SEC))
+ key = _nonce_key(user_id, nonce)
+ client = _get_redis_client()
+ if client is not None:
+ try:
+ ok = await asyncio.wait_for(client.set(key, "1", ex=ttl_sec, nx=True), timeout=2.0)
+ return bool(ok)
+ except (asyncio.TimeoutError, redis_exceptions.RedisError):
+ logger.warning("api crypto nonce redis unavailable, fallback to local memory")
+
+ now = time.time()
+ expire_before = now - ttl_sec
+ stale_keys = [k for k, v in _local_nonce_cache.items() if v < expire_before]
+ for stale in stale_keys:
+ _local_nonce_cache.pop(stale, None)
+ if key in _local_nonce_cache:
+ return False
+ _local_nonce_cache[key] = now
+ return True
+
+
+def _json_error_response(code: int, message: str) -> Response:
+ body = error_response(code, message)
+ return Response(content=json.dumps(body, ensure_ascii=False), media_type="application/json", status_code=200)
+
+
+class ApiCryptoMiddleware(BaseHTTPMiddleware):
+ async def dispatch(self, request: Request, call_next):
+ if not settings.API_ENCRYPT_ENABLED:
+ return await call_next(request)
+ if not settings.API_ENCRYPT_FIXED_SECRET:
+ if settings.API_ENCRYPT_FAIL_OPEN:
+ logger.warning("api crypto bypassed: API_ENCRYPT_FIXED_SECRET is empty")
+ return await call_next(request)
+ return _json_error_response(500, "服务端加密配置缺失")
+ path = request.url.path
+ if not _is_crypto_api(path) or _should_skip_path(path):
+ return await call_next(request)
+
+ content_type = (request.headers.get("content-type") or "").lower()
+ if "multipart/form-data" in content_type:
+ return await call_next(request)
+
+ body_bytes = await request.body()
+ req_json = {}
+ if body_bytes:
+ try:
+ req_json = json.loads(body_bytes.decode("utf-8"))
+ if not isinstance(req_json, dict):
+ req_json = {}
+ except Exception:
+ req_json = {}
+
+ is_encrypted_req = int(req_json.get("enc", 0) or 0) == 1
+ if not is_encrypted_req:
+ logger.info("api crypto bypassed plaintext request: %s", path)
+ return await call_next(request)
+
+ user_id, _ = _claims_from_auth(request.headers.get("authorization"))
+ if not user_id:
+ return _json_error_response(401, "未授权或 token 无效")
+
+ ts = req_json.get("ts")
+ nonce = req_json.get("nonce")
+ edata = req_json.get("edata")
+ if ts is None or not nonce or not isinstance(edata, str):
+ logger.warning("api crypto invalid envelope: path=%s", path)
+ return _json_error_response(400, "加密报文格式错误")
+ if not api_crypto.is_ts_in_window(int(ts), settings.API_ENCRYPT_TS_SKEW_SEC):
+ logger.warning("api crypto ts expired: path=%s user_id=%s", path, user_id)
+ return _json_error_response(400, "请求已过期,请重试")
+ if not await _mark_nonce_once(str(user_id), str(nonce)):
+ logger.warning("api crypto replay detected: path=%s user_id=%s nonce=%s", path, user_id, nonce)
+ return _json_error_response(409, "重复请求")
+
+ key = api_crypto.derive_key(
+ fixed_secret=settings.API_ENCRYPT_FIXED_SECRET,
+ user_id=str(user_id),
+ ts=int(ts),
+ nonce=str(nonce),
+ )
+ try:
+ plain = api_crypto.decrypt_json(edata, key)
+ except Exception as exc:
+ logger.warning("api crypto decrypt failed: path=%s user_id=%s err=%s", path, user_id, exc)
+ return _json_error_response(400, "密文解密失败")
+
+ override_method = request.headers.get("x-http-method-override", "").upper()
+ payload = plain.get("data", plain)
+
+ if override_method == "GET":
+ request.scope["method"] = "GET"
+ query_dict = payload if isinstance(payload, dict) else {}
+ request.scope["query_string"] = urlencode(query_dict, doseq=True).encode("utf-8")
+ body_for_route = b""
+ else:
+ # JSON body 可能是对象(多数 CRUD)或数组(如批量轮播图 POST)
+ if isinstance(payload, (dict, list)):
+ body_for_route = json.dumps(payload, ensure_ascii=False).encode("utf-8")
+ else:
+ body_for_route = json.dumps({}, ensure_ascii=False).encode("utf-8")
+
+ async def receive():
+ return {"type": "http.request", "body": body_for_route, "more_body": False}
+
+ request = Request(request.scope, receive)
+ response = await call_next(request)
+
+ content_type = (response.headers.get("content-type") or "").lower()
+ is_json_response = "application/json" in content_type or response.media_type == "application/json"
+ if not is_json_response:
+ return response
+
+ raw_resp = b""
+ if hasattr(response, "body_iterator"):
+ chunks: list[bytes] = []
+ async for chunk in response.body_iterator:
+ chunks.append(chunk)
+ raw_resp = b"".join(chunks)
+ elif getattr(response, "body", None) is not None:
+ raw_resp = response.body
+ if not raw_resp:
+ return response
+
+ try:
+ resp_obj = json.loads(raw_resp.decode("utf-8"))
+ if not isinstance(resp_obj, dict):
+ return response
+ except Exception:
+ return response
+
+ try:
+ resp_ts = int(time.time() * 1000)
+ resp_nonce = api_crypto.gen_nonce_hex()
+ resp_key = api_crypto.derive_key(
+ fixed_secret=settings.API_ENCRYPT_FIXED_SECRET,
+ user_id=str(user_id),
+ ts=resp_ts,
+ nonce=resp_nonce,
+ )
+ resp_edata = api_crypto.encrypt_json(resp_obj, resp_key)
+ wrapped = {
+ "code": resp_obj.get("code", 200),
+ "encrypted": True,
+ "ts": resp_ts,
+ "nonce": resp_nonce,
+ "edata": resp_edata,
+ }
+ except Exception as exc:
+ logger.warning("api crypto response encrypt failed: path=%s user_id=%s err=%s", path, user_id, exc)
+ if settings.API_ENCRYPT_FAIL_OPEN:
+ return response
+ return _json_error_response(500, "响应加密失败")
+ headers = {k: v for k, v in response.headers.items() if k.lower() != "content-length"}
+ return Response(
+ content=json.dumps(wrapped, ensure_ascii=False),
+ status_code=response.status_code,
+ headers=headers,
+ media_type="application/json",
+ background=response.background,
+ )
diff --git a/backend/app/core/api_request_log_middleware.py b/backend/app/core/api_request_log_middleware.py
new file mode 100644
index 00000000..f2fc1fe8
--- /dev/null
+++ b/backend/app/core/api_request_log_middleware.py
@@ -0,0 +1,276 @@
+"""统一记录 API 请求/响应出入参的中间件"""
+import asyncio
+import json
+import logging
+import time
+from typing import Any, Optional, Tuple
+
+from fastapi import HTTPException
+from jose import jwt
+from starlette.middleware.base import BaseHTTPMiddleware
+from starlette.requests import Request
+from starlette.responses import Response
+
+from app.core.config import settings
+from app.core.database import SessionLocal
+
+logger = logging.getLogger("sqxp")
+
+_SENSITIVE_KEYS = frozenset(
+ {
+ "password",
+ "oldpassword",
+ "newpassword",
+ "encryptedlogin",
+ "authorization",
+ "token",
+ "refreshtoken",
+ "apikey",
+ }
+)
+
+_OMIT_REQ_BYTES = 10 * 1024 * 1024
+
+
+def _truncate(s: str, max_len: int) -> str:
+ if not s:
+ return s
+ if len(s) <= max_len:
+ return s
+ return s[:max_len] + f"\n... [truncated, total {len(s)} chars]"
+
+
+def _redact_obj(obj: Any) -> Any:
+ if isinstance(obj, dict):
+ out = {}
+ for k, v in obj.items():
+ lk = str(k).lower()
+ if (
+ lk in _SENSITIVE_KEYS
+ or "password" in lk
+ or lk in ("encryptedlogin", "refreshtoken")
+ ):
+ out[k] = "***"
+ else:
+ out[k] = _redact_obj(v)
+ return out
+ if isinstance(obj, list):
+ return [_redact_obj(x) for x in obj[:200]] + (["..."] if len(obj) > 200 else [])
+ return obj
+
+
+def _format_body_bytes(raw: bytes, content_type: str, max_bytes: int) -> str:
+ if not raw:
+ return ""
+ if len(raw) > _OMIT_REQ_BYTES:
+ return f"[body omitted: {len(raw)} bytes]"
+ if len(raw) > max_bytes:
+ raw = raw[:max_bytes]
+ ct = (content_type or "").lower()
+ if "multipart/form-data" in ct:
+ return "[multipart/form-data body omitted]"
+ if "application/json" in ct or ct == "" or "text/" in ct:
+ try:
+ text = raw.decode("utf-8", errors="replace")
+ try:
+ parsed = json.loads(text)
+ parsed = _redact_obj(parsed)
+ return _truncate(
+ json.dumps(parsed, ensure_ascii=False),
+ settings.API_REQUEST_LOG_MAX_BODY,
+ )
+ except json.JSONDecodeError:
+ return _truncate(text, settings.API_REQUEST_LOG_MAX_BODY)
+ except Exception:
+ return f"[decode error, {len(raw)} bytes]"
+ return _truncate(raw.decode("utf-8", errors="replace"), settings.API_REQUEST_LOG_MAX_BODY)
+
+
+def _claims_from_auth(auth_header: Optional[str]) -> Tuple[Optional[str], Optional[str], Optional[str]]:
+ if not auth_header or not auth_header.startswith("Bearer "):
+ return None, None, None
+ token = auth_header[7:].strip()
+ if not token:
+ return None, None, None
+ try:
+ payload = jwt.decode(
+ token,
+ settings.SECRET_KEY,
+ algorithms=[settings.ALGORITHM],
+ options={"verify_signature": False, "verify_exp": False},
+ )
+ return payload.get("sub"), payload.get("username"), payload.get("role")
+ except Exception:
+ return None, None, None
+
+
+def _should_skip(request: Request) -> bool:
+ p = request.url.path
+ if request.method == "OPTIONS":
+ return True
+ if p in ("/", "/health", "/docs", "/redoc", "/openapi.json"):
+ return True
+ if p.startswith("/docs") or p.startswith("/redoc"):
+ return True
+ return False
+
+
+def _persist_log(
+ *,
+ http_method: str,
+ path: str,
+ query_string: Optional[str],
+ request_body: Optional[str],
+ response_body: Optional[str],
+ status_code: int,
+ duration_ms: int,
+ user_id: Optional[str],
+ username: Optional[str],
+ role: Optional[str],
+ ip_address: Optional[str],
+ user_agent: Optional[str],
+) -> None:
+ from datetime import datetime
+
+ from app.core.utils import generate_id
+ from app.models.models import SysApiRequestLog
+
+ db = SessionLocal()
+ try:
+ row = SysApiRequestLog(
+ id=generate_id(),
+ http_method=http_method,
+ path=_truncate(path, 500),
+ query_string=_truncate(query_string or "", 2000) or None,
+ request_body=request_body,
+ response_body=response_body,
+ status_code=status_code,
+ duration_ms=duration_ms,
+ user_id=user_id,
+ username=_truncate(username or "", 100) if username else None,
+ role=role,
+ ip_address=ip_address,
+ user_agent=_truncate(user_agent or "", 500) if user_agent else None,
+ is_del=0,
+ create_at=datetime.now(),
+ create_by=None,
+ create_name=None,
+ )
+ db.add(row)
+ db.commit()
+ except Exception as e:
+ logger.warning("api_request_log persist failed: %s", e)
+ db.rollback()
+ finally:
+ db.close()
+
+
+def _schedule_persist_log(**kwargs) -> None:
+ """异步提交日志写入任务,不阻塞当前请求。"""
+ try:
+ loop = asyncio.get_running_loop()
+ except RuntimeError:
+ # 无事件循环时(极少数场景)回退为同步写入,保证日志不丢。
+ try:
+ _persist_log(**kwargs)
+ except Exception as e:
+ logger.warning("api_request_log persist fallback failed: %s", e)
+ return
+
+ task = loop.create_task(asyncio.to_thread(_persist_log, **kwargs))
+
+ def _on_done(t: asyncio.Task) -> None:
+ try:
+ t.result()
+ except Exception as e:
+ logger.warning("api_request_log async task failed: %s", e)
+
+ task.add_done_callback(_on_done)
+
+
+class ApiRequestLogMiddleware(BaseHTTPMiddleware):
+ async def dispatch(self, request: Request, call_next):
+ if not settings.API_REQUEST_LOG_ENABLED or _should_skip(request):
+ return await call_next(request)
+
+ max_b = settings.API_REQUEST_LOG_MAX_BODY_BYTES
+ content_type = request.headers.get("content-type", "")
+
+ try:
+ body = await request.body()
+ except Exception as e:
+ body = b""
+ logger.warning("api_request_log read body failed: %s", e)
+
+ req_text = _format_body_bytes(body, content_type, max_b)
+
+ async def receive():
+ return {"type": "http.request", "body": body, "more_body": False}
+
+ request = Request(request.scope, receive)
+
+ uid, uname, role = _claims_from_auth(request.headers.get("authorization"))
+ ip = request.headers.get("x-forwarded-for", "").split(",")[0].strip()
+ if not ip and request.client:
+ ip = request.client.host
+ ua = request.headers.get("user-agent", "")
+
+ start = time.perf_counter()
+ status_code = 500
+ resp_text = ""
+ raw_resp = b""
+ out: Optional[Response] = None
+ try:
+ response = await call_next(request)
+ status_code = response.status_code
+ if hasattr(response, "body_iterator"):
+ resp_chunks: list[bytes] = []
+ async for chunk in response.body_iterator:
+ resp_chunks.append(chunk)
+ raw_resp = b"".join(resp_chunks)
+ elif getattr(response, "body", None) is not None:
+ raw_resp = response.body
+ ct_out = response.headers.get("content-type", "")
+ resp_text = _format_body_bytes(raw_resp, ct_out, max_b)
+
+ headers = {k: v for k, v in response.headers.items() if k.lower() != "content-length"}
+ out = Response(
+ content=raw_resp,
+ status_code=response.status_code,
+ headers=headers,
+ media_type=response.media_type,
+ background=response.background,
+ )
+ except HTTPException as e:
+ status_code = e.status_code
+ d = e.detail
+ if not isinstance(d, str):
+ try:
+ d = json.dumps(d, ensure_ascii=False)
+ except Exception:
+ d = str(d)
+ resp_text = _truncate(d, settings.API_REQUEST_LOG_MAX_BODY)
+ raise
+ except Exception as e:
+ resp_text = _truncate(str(e), settings.API_REQUEST_LOG_MAX_BODY)
+ raise
+ finally:
+ elapsed_ms = int((time.perf_counter() - start) * 1000)
+ path = request.url.path
+ qs = request.url.query
+ _schedule_persist_log(
+ http_method=request.method,
+ path=path,
+ query_string=qs or None,
+ request_body=req_text or None,
+ response_body=resp_text or None,
+ status_code=status_code,
+ duration_ms=elapsed_ms,
+ user_id=uid,
+ username=uname,
+ role=role,
+ ip_address=ip,
+ user_agent=ua,
+ )
+
+ return out
diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py
new file mode 100644
index 00000000..f5e76455
--- /dev/null
+++ b/backend/app/core/auth.py
@@ -0,0 +1,255 @@
+"""认证模块"""
+import asyncio
+from datetime import datetime, timedelta
+from typing import Optional
+
+import redis.asyncio as redis
+from redis import exceptions as redis_exceptions
+from fastapi import Depends, HTTPException, status, Request
+from fastapi.security import OAuth2PasswordBearer
+from jose import JWTError, jwt
+from sqlalchemy.orm import Session
+
+from app.core.config import settings
+from app.core.database import get_db
+from app.core.security import verify_password
+from app.models.models import SysUser
+from app.crud.user import user_crud
+from app.core.utils import generate_uuid
+
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/public/login")
+
+REFRESH_TOKEN_EXPIRE_DAYS = 7
+REFRESH_TOKEN_EXPIRE_SECONDS = int(timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS).total_seconds())
+
+_redis_client: Optional[redis.Redis] = None
+
+
+def _redis_session_enabled() -> bool:
+ # 只要配置了 REDIS_HOST,就启用“会话校验/refreshToken 吊销”
+ return bool(settings.REDIS_HOST)
+
+
+def _get_redis_client() -> Optional[redis.Redis]:
+ global _redis_client
+ if not _redis_session_enabled():
+ return None
+ if _redis_client is None:
+ _redis_client = redis.Redis(
+ host=settings.REDIS_HOST,
+ port=settings.REDIS_PORT,
+ db=settings.REDIS_DB,
+ password=settings.REDIS_PASSWORD or None,
+ socket_timeout=settings.REDIS_TIMEOUT_SEC,
+ ssl=settings.REDIS_SSL_ENABLED,
+ decode_responses=True,
+ )
+ return _redis_client
+
+
+def _session_key(session_id: str) -> str:
+ return f"auth:sessions:{session_id}"
+
+
+def _refresh_key(refresh_jti: str) -> str:
+ return f"auth:refresh:{refresh_jti}"
+
+
+async def store_session(session_id: str, user_id: str) -> None:
+ """写入会话(用于强制失效)"""
+ r = _get_redis_client()
+ if r is None:
+ return
+ await r.set(_session_key(session_id), user_id, ex=REFRESH_TOKEN_EXPIRE_SECONDS)
+
+
+async def revoke_session(session_id: str) -> None:
+ """撤销会话(登出/强制失效)"""
+ r = _get_redis_client()
+ if r is None:
+ return
+ await r.delete(_session_key(session_id))
+
+
+async def extend_session(session_id: str) -> None:
+ """刷新会话 TTL(用于 refreshToken 延长会话)"""
+ r = _get_redis_client()
+ if r is None:
+ return
+ await r.expire(_session_key(session_id), REFRESH_TOKEN_EXPIRE_SECONDS)
+
+
+async def is_session_valid(session_id: str) -> bool:
+ """检查会话是否仍然有效"""
+ r = _get_redis_client()
+ if r is None:
+ return True
+ if not session_id:
+ return False
+ try:
+ # 添加连接超时保护,Redis 不可用时降级为验证通过
+ return await asyncio.wait_for(r.exists(_session_key(session_id)), timeout=3.0) > 0
+ except (asyncio.TimeoutError, redis_exceptions.TimeoutError, redis_exceptions.ConnectionError):
+ # Redis 连接失败时降级:只验证 JWT,不过滤会话吊销
+ return True
+
+
+async def store_refresh_token(refresh_jti: str, user_id: str) -> None:
+ """写入 refreshToken(用于 one-time 使用/吊销)"""
+ r = _get_redis_client()
+ if r is None:
+ return
+ await r.set(_refresh_key(refresh_jti), user_id, ex=REFRESH_TOKEN_EXPIRE_SECONDS)
+
+
+async def is_refresh_token_valid(refresh_jti: str) -> bool:
+ r = _get_redis_client()
+ if r is None:
+ return True
+ if not refresh_jti:
+ return False
+ try:
+ return await asyncio.wait_for(r.exists(_refresh_key(refresh_jti)), timeout=3.0) > 0
+ except (asyncio.TimeoutError, redis_exceptions.TimeoutError, redis_exceptions.ConnectionError):
+ return True
+
+
+async def revoke_refresh_token(refresh_jti: str) -> None:
+ r = _get_redis_client()
+ if r is None:
+ return
+ await r.delete(_refresh_key(refresh_jti))
+
+
+def create_access_token(
+ data: dict,
+ session_id: str,
+ token_jti: str,
+ expires_delta: Optional[timedelta] = None,
+) -> str:
+ """创建JWT访问令牌"""
+ to_encode = data.copy()
+ if expires_delta:
+ expire = datetime.utcnow() + expires_delta
+ else:
+ expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+ to_encode.update(
+ {
+ "exp": expire,
+ "type": "access",
+ "sid": session_id,
+ "jti": token_jti,
+ }
+ )
+ encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
+ return encoded_jwt
+
+
+def create_refresh_token(data: dict, session_id: str, refresh_jti: str) -> str:
+ """创建刷新令牌"""
+ to_encode = data.copy()
+ expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) # 有效期与 Redis TTL 保持一致
+ to_encode.update(
+ {
+ "exp": expire,
+ "type": "refresh",
+ "sid": session_id,
+ "jti": refresh_jti,
+ }
+ )
+ encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
+ return encoded_jwt
+
+
+def decode_token(token: str) -> dict:
+ """解码JWT令牌"""
+ try:
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
+ return payload
+ except JWTError:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="无效的认证凭据",
+ headers={"WWW-Authenticate": "Bearer"},
+ )
+
+
+async def get_current_user_by_token(
+ token: str,
+ db: Session,
+) -> dict:
+ """根据 token 获取当前用户(包含 Redis 会话校验)"""
+ payload = decode_token(token)
+
+ user_id: str = payload.get("sub")
+ session_id: str = payload.get("sid")
+
+ if not await is_session_valid(session_id):
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="Token无效或已过期",
+ )
+
+ if user_id is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="无效的认证凭据",
+ )
+
+ user = user_crud.get(db, user_id)
+ if not user or user.is_del == 1:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="用户不存在或已删除",
+ )
+
+ if not user.is_active:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="用户已被禁用",
+ )
+
+ return {
+ "user_id": user.id,
+ "username": user.username,
+ "real_name": user.real_name,
+ "role": user.role,
+ "region_id": user.region_id,
+ "phone": getattr(user, "phone", None),
+ "avatar_url": getattr(user, "avatar_url", None),
+ "is_active": getattr(user, "is_active", True),
+ }
+
+
+async def get_current_user(
+ token: str = Depends(oauth2_scheme),
+ db: Session = Depends(get_db)
+) -> dict:
+ """获取当前用户"""
+ return await get_current_user_by_token(token, db)
+
+
+async def get_current_user_simple(token: str = Depends(oauth2_scheme)) -> dict:
+ """简化版获取当前用户(不查数据库)"""
+ payload = decode_token(token)
+ user_id: str = payload.get("sub")
+ if user_id is None:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail="无效的认证凭据",
+ )
+ return {
+ "user_id": user_id,
+ "username": payload.get("username"),
+ "role": payload.get("role"),
+ "region_id": payload.get("region_id"),
+ "real_name": payload.get("real_name")
+ }
+
+
+def get_client_ip(request: Request) -> str:
+ """获取客户端IP"""
+ forwarded = request.headers.get("X-Forwarded-For")
+ if forwarded:
+ return forwarded.split(",")[0].strip()
+ return request.client.host if request.client else "unknown"
diff --git a/backend/app/core/config.py b/backend/app/core/config.py
new file mode 100644
index 00000000..c5d33b90
--- /dev/null
+++ b/backend/app/core/config.py
@@ -0,0 +1,145 @@
+from pydantic_settings import BaseSettings
+from typing import Optional
+from pathlib import Path
+import os
+
+
+class Settings(BaseSettings):
+ """应用配置"""
+
+ # 应用基础配置
+ APP_NAME: str = "中科兴皖一体化门户"
+ DEBUG: bool = True
+ API_V1_PREFIX: str = "/api"
+
+ # 数据库配置
+ DATABASE_URL: str = "mysql+pymysql://root:password@localhost:3306/zkxw"
+
+ # JWT配置
+ SECRET_KEY: str = "your-secret-key-change-in-production-please-use-random-string"
+ ALGORITHM: str = "HS256"
+ ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 * 24 # 30天
+
+ # CORS配置
+ BACKEND_CORS_ORIGINS: list = [
+ "http://localhost:5173", # art-design-pro 开发服务器
+ "http://localhost:8080",
+ "http://localhost:8081", # uni-app H5 端口被占用时常用
+ "http://localhost:8082",
+ "http://127.0.0.1:5173",
+ "http://127.0.0.1:8080",
+ "http://127.0.0.1:8081",
+ "http://127.0.0.1:8082",
+ "http://localhost:3000", # Web开发
+ "http://localhost:3006", # Web开发
+ "http://127.0.0.1:3000",
+ "http://127.0.0.1:3006",
+ ]
+
+ # 文件存储配置
+ STORAGE_TYPE: str = "local" # local, qiniu, oss, minio
+
+ # 本地存储配置
+ LOCAL_UPLOAD_DIR: str = "uploads"
+ LOCAL_STORAGE_URL: str = "/uploads"
+
+ # Redis 配置(用于登录会话与 refreshToken 吊销)
+ # 未配置 REDIS_HOST 时,后端会保持兼容:仍然只校验 JWT,不强制会话吊销。
+ REDIS_HOST: str = ""
+ REDIS_PORT: int = 6379
+ REDIS_DB: int = 0
+ REDIS_PASSWORD: str = ""
+ REDIS_TIMEOUT_SEC: int = 30
+ REDIS_SSL_ENABLED: bool = False
+
+ # 七牛云存储配置 (生产环境推荐)
+ QINIU_ACCESS_KEY: str = ""
+ QINIU_SECRET_KEY: str = ""
+ QINIU_BUCKET: str = ""
+ QINIU_DOMAIN: str = ""
+
+ # 阿里云OSS配置 (可选)
+ OSS_ACCESS_KEY_ID: str = ""
+ OSS_ACCESS_KEY_SECRET: str = ""
+ OSS_BUCKET: str = ""
+ OSS_ENDPOINT: str = ""
+ OSS_DOMAIN: str = ""
+
+ # MinIO配置
+ MINIO_ENDPOINT: str = ""
+ MINIO_ACCESS_KEY: str = ""
+ MINIO_SECRET_KEY: str = ""
+ MINIO_BUCKET: str = ""
+ MINIO_SECURE: bool = False
+ MINIO_BASE_URL: str = ""
+
+ # 微信支付配置 (生产环境必需)
+ WXPAY_MCHID: str = "" # 商户号
+ WXPAY_APPID: str = "" # 公众号AppID
+ WXPAY_MINI_APPID: str = "" # 小程序AppID
+ WXPAY_API_KEY: str = "" # API密钥 (V2)
+ WXPAY_SANDBOX: bool = False # 是否使用沙箱环境
+ WXPAY_NOTIFY_URL: str = "https://your-domain.com/api/payment/notify" # 支付回调地址
+ WXPAY_REFUND_NOTIFY_URL: str = "https://your-domain.com/api/payment/refund-notify" # 退款回调地址
+ # V3 API 证书配置
+ WXPAY_CERT_PATH: str = "" # API证书(apiclient_cert.pem)路径
+ WXPAY_PRIVATE_KEY_PATH: str = "" # API私钥(apiclient_key.pem)路径
+ WXPAY_SERIAL_NO: str = "" # 证书序列号
+
+ # SSL证书配置 (退款必需)
+ WXPAY_SSL_CERT: str = "" # SSL证书路径
+ WXPAY_SSL_KEY: str = "" # SSL私钥路径
+
+ # 微信小程序配置
+ WXPAY_SECRET: str = "" # 小程序Secret
+
+ # 微信模板消息ID配置
+ WX_TEMPLATE_ORDER_STATUS: str = "" # 订单状态通知模板ID
+ WX_TEMPLATE_DELIVERY_NOTICE: str = "" # 配送通知模板ID
+ WX_TEMPLATE_PAYMENT_SUCCESS: str = "" # 支付成功通知模板ID
+ WX_TEMPLATE_REFUND_RESULT: str = "" # 退款结果通知模板ID
+ WX_TEMPLATE_DELIVERY_WARNING: str = "" # 配送时效预警模板ID
+
+ # 登录 RSA:私钥 PEM(多实例/负载均衡须配置同一私钥;留空则进程启动时临时生成,仅适合单机开发)
+ LOGIN_RSA_PRIVATE_KEY_PEM: str = ""
+ # 是否允许明文 username/password 登录(仅建议本地排障临时开启;生产务必 false)
+ LOGIN_ALLOW_PLAINTEXT_LOGIN: bool = False
+ # 加密载荷内时间戳允许偏差(秒),防简单重放
+ LOGIN_ENCRYPT_TS_SKEW_SEC: int = 300
+
+ # 临时文件目录
+ TEMP_FILE_DIR: str = "temp_files"
+
+ # 全量 API 请求日志(中间件写入 sys_api_request_log)
+ API_REQUEST_LOG_ENABLED: bool = True
+ API_REQUEST_LOG_MAX_BODY: int = 65536
+ API_REQUEST_LOG_MAX_BODY_BYTES: int = 131072
+
+ # Web API 请求/响应加密配置
+ API_ENCRYPT_ENABLED: bool = False
+ API_ENCRYPT_FAIL_OPEN: bool = True
+ API_ENCRYPT_FIXED_SECRET: str = ""
+ API_ENCRYPT_TS_SKEW_SEC: int = 300
+ API_ENCRYPT_NONCE_TTL_SEC: int = 300
+ API_ENCRYPT_EXCLUDE_PATHS: str = (
+ "/,"
+ "/health,"
+ "/docs,"
+ "/openapi.json,"
+ "/api/web/auth/login-rsa-public-key,"
+ "/api/web/auth/login,"
+ "/api/web/admin/attachment/upload,"
+ "/api/web/admin/products/upload-image,"
+ "/api/web/admin/categories/upload-image,"
+ "/api/web/admin/users/upload-avatar,"
+ "/api/invoice/download"
+ )
+
+ class Config:
+ # 支持通过 ENV_FILE 指定环境文件;未指定时回退到 backend/.env
+ _default_env_file = Path(__file__).resolve().parents[2] / ".env"
+ env_file = os.getenv("ENV_FILE", str(_default_env_file))
+ case_sensitive = True
+
+
+settings = Settings()
diff --git a/backend/app/core/database.py b/backend/app/core/database.py
new file mode 100644
index 00000000..ebdc57d6
--- /dev/null
+++ b/backend/app/core/database.py
@@ -0,0 +1,26 @@
+from sqlalchemy import create_engine
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+
+from app.core.config import settings
+
+engine = create_engine(
+ settings.DATABASE_URL,
+ pool_pre_ping=True,
+ pool_recycle=3600,
+ echo=settings.DEBUG,
+)
+
+# 避免 commit 后 ORM 实例自动过期,导致读取字段时回表 SELECT(高频接口会显著拖慢)
+SessionLocal = sessionmaker(autocommit=False, autoflush=False, expire_on_commit=False, bind=engine)
+
+Base = declarative_base()
+
+
+def get_db():
+ """获取数据库会话"""
+ db = SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
diff --git a/backend/app/core/exceptions.py b/backend/app/core/exceptions.py
new file mode 100644
index 00000000..f3459669
--- /dev/null
+++ b/backend/app/core/exceptions.py
@@ -0,0 +1,141 @@
+"""错误处理和日志记录模块"""
+import logging
+import traceback
+from typing import Optional, Any
+from datetime import datetime
+from functools import wraps
+
+# 配置日志
+logger = logging.getLogger("sqxp")
+logger.setLevel(logging.INFO)
+
+# 创建控制台处理器
+console_handler = logging.StreamHandler()
+console_handler.setLevel(logging.INFO)
+
+# 创建格式化器
+formatter = logging.Formatter(
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S'
+)
+console_handler.setFormatter(formatter)
+
+# 添加处理器
+if not logger.handlers:
+ logger.addHandler(console_handler)
+
+
+class BusinessException(Exception):
+ """业务异常基类"""
+ def __init__(self, code: int = 400, message: str = "业务处理异常", data: Any = None):
+ self.code = code
+ self.message = message
+ self.data = data
+ super().__init__(message)
+
+
+class ValidationException(BusinessException):
+ """数据验证异常"""
+ def __init__(self, message: str = "数据验证失败", data: Any = None):
+ super().__init__(code=400, message=message, data=data)
+
+
+class PermissionException(BusinessException):
+ """权限不足异常"""
+ def __init__(self, message: str = "权限不足", data: Any = None):
+ super().__init__(code=403, message=message, data=data)
+
+
+class ResourceNotFoundException(BusinessException):
+ """资源不存在异常"""
+ def __init__(self, resource: str = "资源", data: Any = None):
+ super().__init__(code=404, message=f"{resource}不存在", data=data)
+
+
+class ResourceConflictException(BusinessException):
+ """资源冲突异常(如重复创建)"""
+ def __init__(self, message: str = "资源已存在", data: Any = None):
+ super().__init__(code=409, message=message, data=data)
+
+
+class OrderLockedException(BusinessException):
+ """订单已锁定异常"""
+ def __init__(self, message: str = "订单已被锁定", data: Any = None):
+ super().__init__(code=423, message=message, data=data)
+
+
+class InvalidStatusTransitionException(BusinessException):
+ """无效状态转换异常"""
+ def __init__(self, message: str = "无效的状态转换", data: Any = None):
+ super().__init__(code=400, message=message, data=data)
+
+
+def log_operation(module: str, operation: str):
+ """操作日志装饰器"""
+ def decorator(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ start_time = datetime.now()
+ user_id = kwargs.get('current_user', {}).get('user_id') if kwargs.get('current_user') else 'anonymous'
+
+ logger.info(f"[{module}] 用户 {user_id} 开始执行 {operation}")
+
+ try:
+ result = func(*args, **kwargs)
+ elapsed = (datetime.now() - start_time).total_seconds()
+ logger.info(f"[{module}] 用户 {user_id} 执行 {operation} 成功,耗时 {elapsed:.2f}s")
+ return result
+ except BusinessException as e:
+ elapsed = (datetime.now() - start_time).total_seconds()
+ logger.warning(f"[{module}] 用户 {user_id} 执行 {operation} 业务异常: {e.message},耗时 {elapsed:.2f}s")
+ raise
+ except Exception as e:
+ elapsed = (datetime.now() - start_time).total_seconds()
+ logger.error(f"[{module}] 用户 {user_id} 执行 {operation} 系统异常: {str(e)},耗时 {elapsed:.2f}s")
+ logger.error(f"[{module}] 异常详情: {traceback.format_exc()}")
+ raise
+
+ return wrapper
+ return decorator
+
+
+def log_api_request(endpoint: str):
+ """API请求日志装饰器"""
+ def decorator(func):
+ @wraps(func)
+ async def wrapper(*args, **kwargs):
+ start_time = datetime.now()
+ logger.info(f"[API] 请求开始: {endpoint}")
+
+ try:
+ result = await func(*args, **kwargs)
+ elapsed = (datetime.now() - start_time).total_seconds()
+ logger.info(f"[API] 请求完成: {endpoint},耗时 {elapsed:.3f}s")
+ return result
+ except Exception as e:
+ elapsed = (datetime.now() - start_time).total_seconds()
+ logger.error(f"[API] 请求异常: {endpoint},耗时 {elapsed:.3f}s,错误: {str(e)}")
+ raise
+
+ return wrapper
+ return decorator
+
+
+def log_order_operation(operation: str):
+ """订单操作日志装饰器"""
+ return log_operation("订单管理", operation)
+
+
+def log_delivery_operation(operation: str):
+ """配送操作日志装饰器"""
+ return log_operation("配送管理", operation)
+
+
+def log_settlement_operation(operation: str):
+ """结算操作日志装饰器"""
+ return log_operation("结算管理", operation)
+
+
+def log_user_operation(operation: str):
+ """用户操作日志装饰器"""
+ return log_operation("用户管理", operation)
diff --git a/backend/app/core/login_crypto.py b/backend/app/core/login_crypto.py
new file mode 100644
index 00000000..a8d3a8c2
--- /dev/null
+++ b/backend/app/core/login_crypto.py
@@ -0,0 +1,116 @@
+"""登录凭据 RSA 解密(与前端 JSEncrypt PKCS#1 v1.5 兼容)。"""
+from __future__ import annotations
+
+import base64
+import json
+import logging
+import time
+from typing import Any, Dict, Optional, Tuple
+
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import padding, rsa
+
+from app.core.config import settings
+
+logger = logging.getLogger(__name__)
+
+_private_key = None # type: ignore
+_public_pem: Optional[str] = None
+
+
+def ensure_initialized() -> None:
+ """加载或生成 RSA 密钥对。"""
+ global _private_key, _public_pem
+ if _private_key is not None:
+ return
+
+ pem = (settings.LOGIN_RSA_PRIVATE_KEY_PEM or "").strip()
+ # 兼容 .env 中单行保存 PEM(以 "\n" 形式转义换行)的场景
+ if "\\n" in pem and "-----BEGIN" in pem:
+ pem = pem.replace("\\n", "\n")
+ if pem:
+ _private_key = serialization.load_pem_private_key(
+ pem.encode("utf-8"), password=None, backend=default_backend()
+ )
+ logger.info("登录 RSA:已自配置加载私钥")
+ else:
+ _private_key = rsa.generate_private_key(
+ public_exponent=65537, key_size=2048, backend=default_backend()
+ )
+ logger.warning(
+ "登录 RSA:未配置 LOGIN_RSA_PRIVATE_KEY_PEM,已生成进程内临时密钥;"
+ "多进程/负载均衡下请配置固定 PEM。"
+ )
+
+ _public_pem = (
+ _private_key.public_key()
+ .public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+ .decode("utf-8")
+ )
+
+
+def get_public_pem() -> str:
+ ensure_initialized()
+ assert _public_pem is not None
+ return _public_pem
+
+
+def decrypt_login_payload(b64_cipher: str) -> Dict[str, Any]:
+ """Base64 密文 -> JSON 对象(须含 username、password、ts 毫秒时间戳)。"""
+ ensure_initialized()
+ raw = base64.b64decode(b64_cipher)
+ decrypted = _private_key.decrypt(raw, padding.PKCS1v15())
+ text = decrypted.decode("utf-8")
+ return json.loads(text)
+
+
+def _validate_ts(ts_val: Any) -> bool:
+ if ts_val is None:
+ return False
+ try:
+ client_ms = int(ts_val)
+ except (TypeError, ValueError):
+ return False
+ now_ms = int(time.time() * 1000)
+ skew_ms = max(30, int(settings.LOGIN_ENCRYPT_TS_SKEW_SEC)) * 1000
+ return abs(now_ms - client_ms) <= skew_ms
+
+
+def extract_login_credentials(body: dict) -> Tuple[Optional[str], Optional[str], Optional[str]]:
+ """
+ 从请求体解析用户名、密码。
+ 返回 (username, password, error_message);error_message 非空时表示失败。
+ """
+ enc = body.get("encryptedLogin")
+ if isinstance(enc, str) and enc.strip():
+ try:
+ plain = decrypt_login_payload(enc.strip())
+ except Exception:
+ return None, None, "登录凭证无效或已过期"
+
+ if not _validate_ts(plain.get("ts")):
+ return None, None, "登录凭证已过期,请重新登录"
+
+ username = (plain.get("username") or "").strip()
+ password = plain.get("password")
+ if password is None:
+ password = ""
+ elif not isinstance(password, str):
+ password = str(password)
+
+ if not username or not password:
+ return None, None, "用户名和密码不能为空"
+ return username, password, None
+
+ if settings.LOGIN_ALLOW_PLAINTEXT_LOGIN:
+ username = (body.get("username") or "").strip()
+ password = body.get("password") or ""
+ if not username or not password:
+ return None, None, "用户名和密码不能为空"
+ return username, password, None
+
+ return None, None, "请使用加密方式登录"
diff --git a/backend/app/core/menu_guard.py b/backend/app/core/menu_guard.py
new file mode 100644
index 00000000..3a427b08
--- /dev/null
+++ b/backend/app/core/menu_guard.py
@@ -0,0 +1,54 @@
+"""菜单 code 权限守卫"""
+from typing import Callable, Set
+
+from fastapi import Depends, HTTPException
+from sqlalchemy.orm import Session
+
+from app.core.auth import get_current_user
+from app.core.database import get_db
+from app.models.models import SysUserMenuScope
+
+ADMIN_ROLES = {"admin", "super_admin"}
+SUPER_ADMIN_ROLE = "super_admin"
+
+
+def require_admin_or_super_admin(current_user: dict = Depends(get_current_user)):
+ """运营端角色守卫:admin / super_admin 可访问。"""
+ role = current_user.get("role")
+ if role not in ADMIN_ROLES:
+ raise HTTPException(status_code=403, detail="需要运营人员权限")
+ return current_user
+
+
+def get_user_menu_codes(db: Session, user_id: str) -> Set[str]:
+ """读取用户授权的菜单 code 集合。"""
+ if not user_id:
+ return set()
+ rows = (
+ db.query(SysUserMenuScope.menu_code)
+ .filter(
+ SysUserMenuScope.user_id == user_id,
+ SysUserMenuScope.is_del == 0,
+ )
+ .all()
+ )
+ return {str(row[0]).strip() for row in rows if row and row[0]}
+
+
+def require_menu_code(menu_code: str) -> Callable:
+ """接口菜单权限守卫。super_admin 直接放行。"""
+ normalized = (menu_code or "").strip()
+
+ def _dep(
+ current_user: dict = Depends(require_admin_or_super_admin),
+ db: Session = Depends(get_db),
+ ):
+ role = current_user.get("role")
+ if role == SUPER_ADMIN_ROLE:
+ return current_user
+ codes = get_user_menu_codes(db, current_user.get("user_id"))
+ if normalized not in codes:
+ raise HTTPException(status_code=403, detail="无菜单访问权限")
+ return current_user
+
+ return _dep
diff --git a/backend/app/core/order_status_compat.py b/backend/app/core/order_status_compat.py
new file mode 100644
index 00000000..75a2a17d
--- /dev/null
+++ b/backend/app/core/order_status_compat.py
@@ -0,0 +1,99 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from decimal import Decimal
+from typing import Optional
+
+
+@dataclass(frozen=True)
+class ThreeStatus:
+ order_status: str
+ delivery_status: str
+ pay_status: str
+
+
+def _to_decimal(v) -> Decimal:
+ try:
+ if v is None:
+ return Decimal("0")
+ if isinstance(v, Decimal):
+ return v
+ return Decimal(str(v))
+ except Exception:
+ return Decimal("0")
+
+
+def derive_three_status_from_legacy(
+ legacy_status: Optional[str],
+ *,
+ paid_amount=None,
+) -> ThreeStatus:
+ """
+ 阶段A兼容:从旧 status 推导三字段。
+ 约束:旧 status 本身混合了支付/配送/订单生命周期,因此只能尽力推断;
+ 若业务侧已有更精确来源(例如配送任务表/结算表),后续阶段再增强。
+ """
+ s = (legacy_status or "").strip() or "pending_payment"
+ paid = _to_decimal(paid_amount)
+
+ # 默认:仅保留新主状态模型
+ order_status = "pending_confirm" if paid > 0 else "pending_payment"
+ delivery_status = "pending_dispatch"
+ pay_status = "paid" if paid > 0 else "pending_payment"
+
+ if s == "cancelled":
+ order_status = "cancelled"
+ elif s == "completed":
+ order_status = "completed"
+ delivery_status = "delivered"
+ elif s == "pending_payment":
+ order_status = "pending_payment"
+ pay_status = "pending_payment"
+ delivery_status = "pending_dispatch"
+ elif s == "delivering":
+ order_status = "pending_confirm"
+ delivery_status = "delivering"
+ elif s == "pending_confirm":
+ order_status = "pending_confirm"
+ delivery_status = "delivered"
+ elif s == "pending_review_cancel":
+ order_status = "pending_confirm"
+ delivery_status = "pending_dispatch"
+ pass
+ elif s == "refunding":
+ pay_status = "refunding"
+
+ return ThreeStatus(order_status=order_status, delivery_status=delivery_status, pay_status=pay_status)
+
+
+def derive_legacy_status_from_three(
+ *,
+ order_status: Optional[str],
+ delivery_status: Optional[str],
+ pay_status: Optional[str],
+) -> str:
+ """
+ 阶段A兼容:从三字段反推旧 status(用于旧前端/旧筛选参数兼容)。
+ """
+ o = (order_status or "").strip()
+ d = (delivery_status or "").strip()
+ p = (pay_status or "").strip()
+
+ if o == "cancelled":
+ return "cancelled"
+ if p == "refunding":
+ return "refunding"
+ if o == "completed":
+ return "completed"
+
+ if o == "pending_payment":
+ return "pending_payment"
+ if d == "pending_dispatch":
+ return "pending_confirm"
+ if d == "delivering":
+ return "delivering"
+ if d == "delivered":
+ return "pending_confirm"
+
+ return "pending_payment"
+
diff --git a/backend/app/core/request_payload.py b/backend/app/core/request_payload.py
new file mode 100644
index 00000000..fc43ee96
--- /dev/null
+++ b/backend/app/core/request_payload.py
@@ -0,0 +1,71 @@
+"""统一解析接口请求载荷(明文 / 密文信封)。"""
+
+from __future__ import annotations
+
+import logging
+from typing import Any, Union
+
+from app.core import api_crypto
+from app.core.config import settings
+
+
+def resolve_request_payload(
+ request_body: dict,
+ current_user: dict,
+ *,
+ source: str = "unknown",
+) -> Union[dict, list]:
+ """兼容明文与密文信封,返回业务层 payload(多为 dict;批量数组接口解密后为 list)。"""
+ logger = logging.getLogger("sqxp")
+ payload = request_body if isinstance(request_body, dict) else {}
+ if int(payload.get("enc", 0) or 0) == 1 and isinstance(payload.get("edata"), str):
+ logger.warning(
+ "%s received encrypted envelope in route handler; middleware may be bypassed",
+ source,
+ )
+ try:
+ user_id_for_crypto = str(current_user.get("user_id") or "")
+ ts = int(payload.get("ts"))
+ nonce = str(payload.get("nonce") or "")
+ if user_id_for_crypto and nonce and settings.API_ENCRYPT_FIXED_SECRET:
+ key = api_crypto.derive_key(
+ fixed_secret=settings.API_ENCRYPT_FIXED_SECRET,
+ user_id=user_id_for_crypto,
+ ts=ts,
+ nonce=nonce,
+ )
+ plain = api_crypto.decrypt_json(str(payload.get("edata")), key)
+ payload = plain if isinstance(plain, dict) else {}
+ logger.warning(
+ "%s decrypted payload via route fallback; please verify ApiCryptoMiddleware",
+ source,
+ )
+ except Exception as exc:
+ logger.warning("%s resolve_request_payload decrypt failed: err=%s", source, exc)
+ return {}
+ dw = source.startswith("delivery_warning")
+ if isinstance(payload, dict):
+ inner = payload.get("data")
+ if isinstance(inner, (dict, list)):
+ if dw:
+ if isinstance(inner, dict):
+ logger.info(
+ "%s unwrap: using inner dict keys=%s note_len=%s remark_len=%s",
+ source,
+ sorted(inner.keys()),
+ len(str(inner.get("note") or "")),
+ len(str(inner.get("remark") or "")),
+ )
+ else:
+ logger.info("%s unwrap: inner is list len=%s", source, len(inner))
+ return inner
+ if dw:
+ logger.info(
+ "%s no_inner_data: payload_keys=%s (no dict/list data wrapper)",
+ source,
+ sorted(payload.keys()),
+ )
+ elif dw:
+ logger.info("%s payload_not_dict type=%s", source, type(payload).__name__)
+ return payload if isinstance(payload, dict) else {}
+
diff --git a/backend/app/core/response.py b/backend/app/core/response.py
new file mode 100644
index 00000000..22b805be
--- /dev/null
+++ b/backend/app/core/response.py
@@ -0,0 +1,47 @@
+"""统一响应格式"""
+from typing import Any, Generic, Optional, TypeVar, List
+from pydantic import BaseModel, Field
+
+T = TypeVar("T")
+
+
+class ResponseModel(BaseModel, Generic[T]):
+ """通用响应格式"""
+ code: int = 200
+ message: str = "success"
+ data: Optional[T] = None
+ timestamp: int = 0
+
+ class Config:
+ from_attributes = True
+
+
+class PageResult(BaseModel, Generic[T]):
+ """分页响应格式"""
+ items: List[T] = Field(default_factory=list, serialization_alias="list")
+ total: int = 0
+ page: int = 1
+ pageSize: int = 10
+ totalPages: int = 0
+
+
+def success_response(data: Any = None, message: str = "success") -> dict:
+ """成功响应"""
+ from datetime import datetime
+ return {
+ "code": 200,
+ "message": message,
+ "data": data,
+ "timestamp": int(datetime.now().timestamp() * 1000)
+ }
+
+
+def error_response(code: int = 400, message: str = "error", data: Any = None) -> dict:
+ """错误响应"""
+ from datetime import datetime
+ return {
+ "code": code,
+ "message": message,
+ "data": data,
+ "timestamp": int(datetime.now().timestamp() * 1000)
+ }
diff --git a/backend/app/core/security.py b/backend/app/core/security.py
new file mode 100644
index 00000000..dc48435d
--- /dev/null
+++ b/backend/app/core/security.py
@@ -0,0 +1,22 @@
+import bcrypt
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+ """验证密码(哈希非 bcrypt 或损坏时返回 False,避免抛错)"""
+ if not plain_password or not hashed_password:
+ return False
+ hp = hashed_password if isinstance(hashed_password, str) else str(hashed_password)
+ if not hp.startswith("$2"):
+ return False
+ try:
+ return bcrypt.checkpw(
+ plain_password.encode("utf-8"),
+ hp.encode("utf-8"),
+ )
+ except (ValueError, TypeError):
+ return False
+
+
+def get_password_hash(password: str) -> str:
+ """生成密码哈希"""
+ return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
diff --git a/backend/app/core/storage.py b/backend/app/core/storage.py
new file mode 100644
index 00000000..e147c10e
--- /dev/null
+++ b/backend/app/core/storage.py
@@ -0,0 +1,335 @@
+"""文件存储工具模块
+
+支持多种存储方式:
+1. 本地存储 (local) - 默认
+2. 七牛云存储 (qiniu) - 推荐生产环境使用
+3. 阿里云OSS (oss) - 可选
+
+使用方式:
+ from app.core.storage import storage
+
+ # 上传文件
+ url = storage.upload_file(file_content, "test.jpg", "images")
+
+ # 上传 Base64 图片
+ url = storage.upload_base64(base64_data, "avatar.png", "avatar")
+
+ # 删除文件
+ storage.delete_file("images/test.jpg")
+"""
+
+import os
+import uuid
+import base64
+from io import BytesIO
+from datetime import datetime
+from typing import Optional, BinaryIO
+from pathlib import Path
+
+from app.core.config import settings
+
+
+# HEIC 格式的 MIME 类型
+HEIC_TYPES = {"image/heic", "image/heif"}
+
+
+def convert_heic_to_jpeg(content: bytes) -> bytes:
+ """将 HEIC/HEIF 格式转换为 JPEG 格式"""
+ try:
+ import pillow_heif
+ from PIL import Image
+
+ heif_file = pillow_heif.open(BytesIO(content))
+ image = heif_file.to_pillow()
+ output = BytesIO()
+ image.save(output, format="JPEG", quality=85)
+ return output.getvalue()
+ except ImportError:
+ raise Exception("请安装 pillow-heif: pip install pillow-heif")
+
+
+def process_image_content(content: bytes, filename: str) -> tuple[bytes, str]:
+ """处理图片内容,如果是 HEIC 格式则转换为 JPEG"""
+ import imghdr
+ img_type = imghdr.what(None, h=content[:32])
+ if img_type in ("heic", "heif"):
+ return convert_heic_to_jpeg(content), filename.rsplit(".", 1)[0] + ".jpg"
+ return content, filename
+
+
+class BaseStorage:
+ """存储基类"""
+
+ def upload_file(self, file_content: bytes, filename: str, folder: str = "") -> str:
+ """上传文件"""
+ raise NotImplementedError
+
+ def upload_base64(self, base64_data: str, filename: str, folder: str = "") -> str:
+ """上传 Base64 编码的图片"""
+ raise NotImplementedError
+
+ def delete_file(self, file_path: str) -> bool:
+ """删除文件"""
+ raise NotImplementedError
+
+ def get_url(self, file_path: str) -> str:
+ """获取文件访问URL"""
+ raise NotImplementedError
+
+
+class LocalStorage(BaseStorage):
+ """本地存储"""
+
+ def __init__(self):
+ self.base_url = settings.LOCAL_STORAGE_URL.rstrip("/")
+ self.upload_dir = settings.LOCAL_UPLOAD_DIR
+
+ def _ensure_dir(self, folder: str) -> Path:
+ """确保目录存在"""
+ path = Path(self.upload_dir) / folder / datetime.now().strftime("%Y%m")
+ path.mkdir(parents=True, exist_ok=True)
+ return path
+
+ def _generate_filename(self, filename: str) -> str:
+ """生成唯一文件名"""
+ ext = os.path.splitext(filename)[1] or ""
+ unique_name = f"{uuid.uuid4().hex}{ext}"
+ return unique_name
+
+ def upload_file(self, file_content: bytes, filename: str, folder: str = "") -> str:
+ """上传文件到本地"""
+ # 处理 HEIC 格式图片
+ file_content, filename = process_image_content(file_content, filename)
+
+ folder_path = self._ensure_dir(folder)
+ unique_filename = self._generate_filename(filename)
+ file_path = folder_path / unique_filename
+
+ with open(file_path, "wb") as f:
+ f.write(file_content)
+
+ relative_path = f"{folder}/{datetime.now().strftime('%Y%m')}/{unique_filename}" if folder else f"{datetime.now().strftime('%Y%m')}/{unique_filename}"
+ return self.get_url(relative_path)
+
+ def upload_base64(self, base64_data: str, filename: str, folder: str = "") -> str:
+ """上传 Base64 图片"""
+ # 去除 data:image/png;base64, 等前缀
+ if "," in base64_data:
+ base64_data = base64_data.split(",")[1]
+
+ file_content = base64.b64decode(base64_data)
+ return self.upload_file(file_content, filename, folder)
+
+ def delete_file(self, file_path: str) -> bool:
+ """删除本地文件"""
+ try:
+ # 移除 URL 前缀获取相对路径
+ if file_path.startswith(self.base_url):
+ relative_path = file_path.replace(self.base_url, "").lstrip("/")
+ else:
+ relative_path = file_path.lstrip("/")
+
+ file_full_path = Path(self.upload_dir) / relative_path
+ if file_full_path.exists():
+ file_full_path.unlink()
+ return True
+ return False
+ except Exception:
+ return False
+
+ def get_url(self, file_path: str) -> str:
+ """获取文件访问URL"""
+ return f"{self.base_url}/{file_path}"
+
+
+class QiniuStorage(BaseStorage):
+ """七牛云存储"""
+
+ def __init__(self):
+ try:
+ from qiniu import Auth, put_data, delete
+ self.qiniu = Auth(settings.QINIU_ACCESS_KEY, settings.QINIU_SECRET_KEY)
+ self.bucket = settings.QINIU_BUCKET
+ self.base_url = settings.QINIU_DOMAIN.rstrip("/")
+ except ImportError:
+ raise ImportError("请安装七牛云 SDK: pip install qiniu")
+
+ def _generate_filename(self, filename: str) -> str:
+ """生成唯一文件名"""
+ ext = os.path.splitext(filename)[1] or ""
+ unique_name = f"{datetime.now().strftime('%Y%m/%d')}/{uuid.uuid4().hex}{ext}"
+ return unique_name
+
+ def upload_file(self, file_content: bytes, filename: str, folder: str = "") -> str:
+ """上传文件到七牛云"""
+ from qiniu import put_data
+
+ # 处理 HEIC 格式图片
+ file_content, filename = process_image_content(file_content, filename)
+
+ key = self._generate_filename(filename)
+ token = self.qiniu.upload_token(self.bucket, key, 3600)
+
+ ret, info = put_data(token, key, file_content)
+ if ret is not None:
+ return self.get_url(ret["key"])
+ raise Exception(f"七牛云上传失败: {info}")
+
+ def upload_base64(self, base64_data: str, filename: str, folder: str = "") -> str:
+ """上传 Base64 图片到七牛云"""
+ if "," in base64_data:
+ base64_data = base64_data.split(",")[1]
+
+ file_content = base64.b64decode(base64_data)
+ return self.upload_file(file_content, filename, folder)
+
+ def delete_file(self, file_path: str) -> bool:
+ """删除七牛云文件"""
+ from qiniu import delete
+
+ try:
+ key = file_path.replace(f"{self.base_url}/", "")
+ ret, info = delete(self.bucket, key)
+ return ret is not None
+ except Exception:
+ return False
+
+ def get_url(self, file_path: str) -> str:
+ """获取文件访问URL"""
+ return f"{self.base_url}/{file_path}"
+
+
+class OSSStorage(BaseStorage):
+ """阿里云OSS存储"""
+
+ def __init__(self):
+ try:
+ import oss2
+ self.auth = oss2.Auth(settings.OSS_ACCESS_KEY_ID, settings.OSS_ACCESS_KEY_SECRET)
+ self.bucket_name = settings.OSS_BUCKET
+ self.bucket = oss2.Bucket(self.auth, settings.OSS_ENDPOINT, self.bucket_name)
+ self.base_url = settings.OSS_DOMAIN.rstrip("/")
+ except ImportError:
+ raise ImportError("请安装阿里云 OSS SDK: pip install oss2")
+
+ def _generate_filename(self, filename: str) -> str:
+ """生成唯一文件名"""
+ ext = os.path.splitext(filename)[1] or ""
+ unique_name = f"{datetime.now().strftime('%Y%m/%d')}/{uuid.uuid4().hex}{ext}"
+ return unique_name
+
+ def upload_file(self, file_content: bytes, filename: str, folder: str = "") -> str:
+ """上传文件到OSS"""
+ # 处理 HEIC 格式图片
+ file_content, filename = process_image_content(file_content, filename)
+
+ key = self._generate_filename(filename)
+ result = self.bucket.put_object(key, file_content)
+ if result.status == 200:
+ return self.get_url(key)
+ raise Exception(f"OSS上传失败: {result.status}")
+
+ def upload_base64(self, base64_data: str, filename: str, folder: str = "") -> str:
+ """上传 Base64 图片到OSS"""
+ if "," in base64_data:
+ base64_data = base64_data.split(",")[1]
+
+ file_content = base64.b64decode(base64_data)
+ return self.upload_file(file_content, filename, folder)
+
+ def delete_file(self, file_path: str) -> bool:
+ """删除OSS文件"""
+ try:
+ key = file_path.replace(f"{self.base_url}/", "")
+ result = self.bucket.delete_object(key)
+ return result.status == 204
+ except Exception:
+ return False
+
+ def get_url(self, file_path: str) -> str:
+ """获取文件访问URL"""
+ return f"{self.base_url}/{file_path}"
+
+
+class MinIOStorage(BaseStorage):
+ """MinIO存储"""
+
+ def __init__(self):
+ try:
+ from minio import Minio
+ self.client = Minio(
+ settings.MINIO_ENDPOINT,
+ access_key=settings.MINIO_ACCESS_KEY,
+ secret_key=settings.MINIO_SECRET_KEY,
+ secure=settings.MINIO_SECURE,
+ )
+ self.bucket_name = settings.MINIO_BUCKET
+ if settings.MINIO_BASE_URL:
+ self.base_url = settings.MINIO_BASE_URL.rstrip("/")
+ else:
+ scheme = "https" if settings.MINIO_SECURE else "http"
+ self.base_url = f"{scheme}://{settings.MINIO_ENDPOINT}/{self.bucket_name}"
+ except ImportError:
+ raise ImportError("请安装 MinIO SDK: pip install minio")
+
+ def _generate_filename(self, filename: str) -> str:
+ """生成唯一文件名"""
+ ext = os.path.splitext(filename)[1] or ""
+ unique_name = f"{datetime.now().strftime('%Y%m/%d')}/{uuid.uuid4().hex}{ext}"
+ return unique_name
+
+ def upload_file(self, file_content: bytes, filename: str, folder: str = "") -> str:
+ """上传文件到MinIO"""
+ # 处理 HEIC 格式图片
+ file_content, filename = process_image_content(file_content, filename)
+
+ key = self._generate_filename(filename)
+ if folder:
+ key = f"{folder.strip('/')}/{key}"
+ data = BytesIO(file_content)
+ self.client.put_object(
+ bucket_name=self.bucket_name,
+ object_name=key,
+ data=data,
+ length=len(file_content),
+ )
+ return self.get_url(key)
+
+ def upload_base64(self, base64_data: str, filename: str, folder: str = "") -> str:
+ """上传 Base64 图片到MinIO"""
+ if "," in base64_data:
+ base64_data = base64_data.split(",")[1]
+
+ file_content = base64.b64decode(base64_data)
+ return self.upload_file(file_content, filename, folder)
+
+ def delete_file(self, file_path: str) -> bool:
+ """删除MinIO文件"""
+ try:
+ key = file_path.replace(f"{self.base_url}/", "")
+ self.client.remove_object(self.bucket_name, key)
+ return True
+ except Exception:
+ return False
+
+ def get_url(self, file_path: str) -> str:
+ """获取文件访问URL"""
+ return f"{self.base_url}/{file_path}"
+
+
+def get_storage() -> BaseStorage:
+ """获取存储实例"""
+ storage_type = settings.STORAGE_TYPE.lower()
+
+ if storage_type == "qiniu":
+ return QiniuStorage()
+ elif storage_type == "oss":
+ return OSSStorage()
+ elif storage_type == "minio":
+ return MinIOStorage()
+ else:
+ return LocalStorage()
+
+
+# 全局存储实例
+storage = get_storage()
diff --git a/backend/app/core/utils.py b/backend/app/core/utils.py
new file mode 100644
index 00000000..8a817eb2
--- /dev/null
+++ b/backend/app/core/utils.py
@@ -0,0 +1,56 @@
+"""统一工具函数"""
+import uuid
+import random
+import string
+from datetime import datetime
+from typing import Optional
+
+
+def generate_uuid() -> str:
+ """生成UUID"""
+ return str(uuid.uuid4()).replace("-", "").upper()
+
+
+def generate_id(prefix: str = "") -> str:
+ """生成带前缀的ID"""
+ uid = str(uuid.uuid4()).replace("-", "").upper()
+ return f"{prefix}{uid}" if prefix else uid
+
+
+def generate_random_password(length: int = 8) -> str:
+ """生成随机密码"""
+ chars = string.ascii_letters + string.digits
+ return ''.join(random.choice(chars) for _ in range(length))
+
+
+def get_current_time() -> datetime:
+ """获取当前时间"""
+ return datetime.now()
+
+
+def format_datetime(dt: Optional[datetime]) -> Optional[str]:
+ """格式化时间为ISO 8601格式"""
+ if dt is None:
+ return None
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
+
+
+def parse_datetime(dt_str: str) -> Optional[datetime]:
+ """解析ISO 8601格式时间"""
+ if not dt_str:
+ return None
+ try:
+ return datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
+ except (ValueError, AttributeError):
+ return None
+
+
+def get_file_size_display(size_bytes: int) -> str:
+ """将字节数转换为可读的文件大小"""
+ if size_bytes is None:
+ return "0 B"
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
+ if size_bytes < 1024.0:
+ return f"{size_bytes:.2f} {unit}"
+ size_bytes /= 1024.0
+ return f"{size_bytes:.2f} PB"
diff --git a/backend/app/core/wechat_message.py b/backend/app/core/wechat_message.py
new file mode 100644
index 00000000..9a0817cd
--- /dev/null
+++ b/backend/app/core/wechat_message.py
@@ -0,0 +1,354 @@
+"""微信消息通知服务
+
+支持微信小程序模板消息推送
+
+使用方式:
+ from app.core.wechat_message import WechatMessageService
+
+ # 发送订单状态通知
+ WechatMessageService.send_order_status(
+ openid="user_openid",
+ order_no="ORD202603190001",
+ status="已支付",
+ amount=100.00
+ )
+
+ # 发送配送通知
+ WechatMessageService.send_delivery_notice(
+ openid="deliver_openid",
+ task_no="DLV20260319001",
+ address="深圳市南山区...",
+ time="尽快取货"
+ )
+"""
+
+import json
+import logging
+from typing import Optional, Dict, Any
+from datetime import datetime
+
+import requests
+
+from app.core.config import settings
+
+logger = logging.getLogger(__name__)
+
+
+class WechatMessageService:
+ """微信消息服务"""
+
+ # 模板消息ID配置
+ TEMPLATE_IDS = {
+ "order_status": "ORDER_STATUS_TEMPLATE_ID", # 订单状态通知
+ "delivery_notice": "DELIVERY_NOTICE_TEMPLATE_ID", # 配送通知
+ "payment_success": "PAYMENT_SUCCESS_TEMPLATE_ID", # 支付成功通知
+ "refund_result": "REFUND_RESULT_TEMPLATE_ID", # 退款结果通知
+ "delivery_warning": "DELIVERY_WARNING_TEMPLATE_ID", # 配送时效预警
+ }
+
+ def __init__(self):
+ self.appid = settings.WXPAY_MINI_APPID
+ self.secret = settings.WXPAY_SECRET
+ self.access_token = None
+ self.token_expires_at = None
+
+ def _get_access_token(self) -> Optional[str]:
+ """获取Access Token"""
+ if self.access_token and self.token_expires_at:
+ if datetime.now() < self.token_expires_at:
+ return self.access_token
+
+ try:
+ url = "https://api.weixin.qq.com/cgi-bin/token"
+ params = {
+ "grant_type": "client_credential",
+ "appid": self.appid,
+ "secret": self.secret
+ }
+
+ response = requests.get(url, params=params, timeout=10)
+ result = response.json()
+
+ if "access_token" in result:
+ self.access_token = result["access_token"]
+ expires_in = result.get("expires_in", 7200)
+ self.token_expires_at = datetime.now().timestamp() + expires_in - 300
+ return self.access_token
+ else:
+ logger.error(f"获取Access Token失败: {result}")
+ return None
+
+ except Exception as e:
+ logger.error(f"请求Access Token异常: {str(e)}")
+ return None
+
+ def _send_template_message(self, openid: str, template_id: str, data: Dict, page: str = None, color: str = None) -> Dict:
+ """发送模板消息
+
+ Args:
+ openid: 用户openid
+ template_id: 模板ID
+ data: 模板数据
+ page: 跳转页面
+ color: 字体颜色
+
+ Returns:
+ Dict: 发送结果
+ """
+ access_token = self._get_access_token()
+ if not access_token:
+ return {"success": False, "error": "获取access_token失败"}
+
+ try:
+ url = f"https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={access_token}"
+
+ payload = {
+ "touser": openid,
+ "template_id": template_id,
+ "data": data
+ }
+
+ if page:
+ payload["miniprogram"] = {
+ "appid": self.appid,
+ "pagepath": page
+ }
+
+ if color:
+ payload["color"] = color
+
+ response = requests.post(url, json=payload, timeout=10)
+ result = response.json()
+
+ if result.get("errcode") == 0:
+ logger.info(f"模板消息发送成功: {openid}, msgid: {result.get('msgid')}")
+ return {"success": True, "msgid": result.get("msgid")}
+ else:
+ logger.error(f"模板消息发送失败: {result}")
+ return {"success": False, "error": result.get("errmsg", "发送失败")}
+
+ except Exception as e:
+ logger.error(f"发送模板消息异常: {str(e)}")
+ return {"success": False, "error": str(e)}
+
+ @classmethod
+ def send_order_status(
+ cls,
+ openid: str,
+ order_no: str,
+ status: str,
+ amount: float,
+ remark: str = None
+ ) -> Dict:
+ """发送订单状态通知
+
+ Args:
+ openid: 用户openid
+ order_no: 订单号
+ status: 订单状态
+ amount: 订单金额
+ remark: 备注
+ """
+ template_id = cls.TEMPLATE_IDS.get("order_status")
+ if not template_id:
+ return {"success": False, "error": "未配置订单状态模板ID"}
+
+ data = {
+ "order_no": {"value": order_no, "color": "#173177"},
+ "status": {"value": status, "color": "#07c160"},
+ "amount": {"value": f"¥{amount:.2f}", "color": "#ff6600"},
+ "remark": {"value": remark or "感谢您的使用", "color": "#888888"}
+ }
+
+ service = cls()
+ return service._send_template_message(
+ openid=openid,
+ template_id=template_id,
+ data=data,
+ page="pages/client/order/detail/detail"
+ )
+
+ @classmethod
+ def send_payment_success(
+ cls,
+ openid: str,
+ order_no: str,
+ amount: float,
+ payment_method: str = "微信支付"
+ ) -> Dict:
+ """发送支付成功通知
+
+ Args:
+ openid: 用户openid
+ order_no: 订单号
+ amount: 支付金额
+ payment_method: 支付方式
+ """
+ template_id = cls.TEMPLATE_IDS.get("payment_success")
+ if not template_id:
+ return {"success": False, "error": "未配置支付成功模板ID"}
+
+ data = {
+ "order_no": {"value": order_no, "color": "#173177"},
+ "amount": {"value": f"¥{amount:.2f}", "color": "#ff6600"},
+ "payment_method": {"value": payment_method, "color": "#07c160"},
+ "time": {"value": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "color": "#888888"}
+ }
+
+ service = cls()
+ return service._send_template_message(
+ openid=openid,
+ template_id=template_id,
+ data=data,
+ page="pages/client/order/detail/detail"
+ )
+
+ @classmethod
+ def send_delivery_notice(
+ cls,
+ openid: str,
+ task_no: str,
+ supplier_name: str,
+ address: str,
+ remark: str = None
+ ) -> Dict:
+ """发送配送通知
+
+ Args:
+ openid: 配送员openid
+ task_no: 配送单号
+ supplier_name: 供应商名称
+ address: 取货地址
+ remark: 备注
+ """
+ template_id = cls.TEMPLATE_IDS.get("delivery_notice")
+ if not template_id:
+ return {"success": False, "error": "未配置配送通知模板ID"}
+
+ data = {
+ "task_no": {"value": task_no, "color": "#173177"},
+ "supplier": {"value": supplier_name, "color": "#07c160"},
+ "address": {"value": address, "color": "#173177"},
+ "remark": {"value": remark or "请尽快前往取货", "color": "#ff6600"}
+ }
+
+ service = cls()
+ return service._send_template_message(
+ openid=openid,
+ template_id=template_id,
+ data=data,
+ page="pages/deliver/task/detail"
+ )
+
+ @classmethod
+ def send_delivery_warning(
+ cls,
+ openid: str,
+ task_no: str,
+ remaining_minutes: int,
+ urgency_level: str = "紧急"
+ ) -> Dict:
+ """发送配送时效预警
+
+ Args:
+ openid: 配送员openid
+ task_no: 配送单号
+ remaining_minutes: 剩余分钟数
+ urgency_level: 紧急程度
+ """
+ template_id = cls.TEMPLATE_IDS.get("delivery_warning")
+ if not template_id:
+ return {"success": False, "error": "未配置配送预警模板ID"}
+
+ color = "#ff0000" if urgency_level == "紧急" else "#ff6600"
+
+ data = {
+ "task_no": {"value": task_no, "color": "#173177"},
+ "remaining": {"value": f"剩余{remaining_minutes}分钟", "color": color},
+ "level": {"value": urgency_level, "color": color},
+ "time": {"value": datetime.now().strftime("%H:%M"), "color": "#888888"}
+ }
+
+ service = cls()
+ return service._send_template_message(
+ openid=openid,
+ template_id=template_id,
+ data=data,
+ page="pages/deliver/task/detail"
+ )
+
+ @classmethod
+ def send_refund_result(
+ cls,
+ openid: str,
+ order_no: str,
+ refund_amount: float,
+ refund_time: str = None,
+ reason: str = None
+ ) -> Dict:
+ """发送退款结果通知
+
+ Args:
+ openid: 用户openid
+ order_no: 订单号
+ refund_amount: 退款金额
+ refund_time: 退款时间
+ reason: 退款原因
+ """
+ template_id = cls.TEMPLATE_IDS.get("refund_result")
+ if not template_id:
+ return {"success": False, "error": "未配置退款结果模板ID"}
+
+ data = {
+ "order_no": {"value": order_no, "color": "#173177"},
+ "amount": {"value": f"¥{refund_amount:.2f}", "color": "#ff6600"},
+ "time": {"value": refund_time or datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "color": "#888888"},
+ "reason": {"value": reason or "原路退回", "color": "#07c160"}
+ }
+
+ service = cls()
+ return service._send_template_message(
+ openid=openid,
+ template_id=template_id,
+ data=data,
+ page="pages/client/refund/refund"
+ )
+
+ @classmethod
+ def send_urge_notification(
+ cls,
+ deliver_openid: str,
+ order_no: str,
+ client_name: str,
+ address: str
+ ) -> Dict:
+ """发送催单通知给配送员
+
+ Args:
+ deliver_openid: 配送员openid
+ order_no: 订单号
+ client_name: 客户名称
+ address: 配送地址
+ """
+ template_id = cls.TEMPLATE_IDS.get("delivery_notice")
+ if not template_id:
+ return {"success": False, "error": "未配置配送通知模板ID"}
+
+ data = {
+ "task_no": {"value": order_no, "color": "#173177"},
+ "supplier": {"value": f"客户:{client_name}", "color": "#07c160"},
+ "address": {"value": address, "color": "#173177"},
+ "remark": {"value": "⚠️ 客户催单,请尽快处理", "color": "#ff0000"}
+ }
+
+ service = cls()
+ return service._send_template_message(
+ openid=deliver_openid,
+ template_id=template_id,
+ data=data,
+ page="pages/deliver/task/detail"
+ )
+
+
+# 全局实例
+wechat_message = WechatMessageService()
diff --git a/backend/app/core/wechat_pay.py b/backend/app/core/wechat_pay.py
new file mode 100644
index 00000000..26108a1a
--- /dev/null
+++ b/backend/app/core/wechat_pay.py
@@ -0,0 +1,627 @@
+"""微信支付工具模块
+
+支持微信支付 V3 API
+文档参考: https://pay.weixin.qq.com/docs/v3/apis/index.html
+
+使用方式:
+ from app.core.wechat_pay import wechat_pay, WechatPayClient
+
+ # 统一下单
+ result = wechat_pay.unified_order(out_trade_no, total_amount, description, notify_url)
+
+ # 申请退款
+ result = wechat_pay.refund(transaction_id, refund_amount, total_amount)
+"""
+
+import hashlib
+import time
+import random
+import string
+import json
+import base64
+import xml.etree.ElementTree as ET
+from typing import Optional, Dict, Any
+from datetime import datetime
+from urllib.parse import urljoin
+import ssl
+import urllib.request
+import urllib.error
+
+from app.core.config import settings
+
+
+class WechatPayClient:
+ """微信支付客户端"""
+
+ def __init__(self):
+ self.mchid = settings.WXPAY_MCHID
+ self.appid = settings.WXPAY_APPID
+ self.api_key = settings.WXPAY_API_KEY
+ self.cert_path = settings.WXPAY_CERT_PATH
+ self.private_key_path = settings.WXPAY_PRIVATE_KEY_PATH
+ self.sandbox = settings.WXPAY_SANDBOX
+
+ # API 版本
+ self.api_base = "https://api.mchpay.qpic.cn/v3" if self.sandbox else "https://api.mchpay.qpic.cn/v3"
+
+ def _generate_nonce_str(self, length: int = 32) -> str:
+ """生成随机字符串"""
+ return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
+
+ def _generate_sign(self, data: Dict[str, Any]) -> str:
+ """生成签名 (V2 API)"""
+ # V2 签名算法
+ sorted_keys = sorted(data.keys())
+ sign_str = "&".join([f"{k}={data[k]}" for k in sorted_keys if data[k]])
+ sign_str += f"&key={self.api_key}"
+ return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
+
+ def _generate_sign_v3(self, method: str, url: str, timestamp: str, nonce: str, body: str = "") -> str:
+ """生成签名 V3 (SHA256 with RSA)"""
+ try:
+ from cryptography.hazmat.primitives import hashes, serialization
+ from cryptography.hazmat.primitives.asymmetric import padding
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.x509 import load_pem_x509_certificate
+
+ with open(self.private_key_path, 'r') as f:
+ private_key = f.read()
+
+ # 构建签名串
+ sign_str = f"{method}\n{url}\n{timestamp}\n{nonce}\n{body}\n"
+
+ # 使用私钥签名
+ from cryptography.hazmat.primitives import hashes
+ from cryptography.hazmat.primitives.asymmetric import padding
+
+ key = serialization.load_pem_private_key(
+ private_key.encode(),
+ password=None,
+ backend=default_backend()
+ )
+
+ signature = key.sign(
+ sign_str.encode('utf-8'),
+ padding.PKCS1v15(),
+ hashes.SHA256()
+ )
+
+ return base64.b64encode(signature).decode('utf-8')
+ except ImportError:
+ raise ImportError("请安装 cryptography: pip install cryptography")
+
+ def _make_request(self, url: str, method: str = "GET", data: Dict = None, need_cert: bool = False) -> Dict:
+ """发送HTTP请求"""
+ headers = {
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'SQXP/1.0'
+ }
+
+ body = json.dumps(data) if data else ""
+
+ try:
+ if need_cert and self.cert_path and self.private_key_path:
+ import ssl
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.load_cert_chain(self.cert_path, self.private_key_path)
+ req = urllib.request.Request(url, data=body.encode(), headers=headers, method=method)
+ with urllib.request.urlopen(req, context=context) as response:
+ return json.loads(response.read().decode())
+ else:
+ req = urllib.request.Request(url, data=body.encode(), headers=headers, method=method)
+ with urllib.request.urlopen(req) as response:
+ return json.loads(response.read().decode())
+ except urllib.error.HTTPError as e:
+ error_body = e.read().decode() if e.fp else ""
+ try:
+ return json.loads(error_body)
+ except:
+ return {"error": error_body, "code": e.code}
+ except Exception as e:
+ return {"error": str(e), "code": -1}
+
+ def unified_order(
+ self,
+ out_trade_no: str,
+ total_amount: int,
+ description: str,
+ notify_url: str,
+ openid: str = None
+ ) -> Dict[str, Any]:
+ """
+ 统一下单接口 (V3)
+
+ Args:
+ out_trade_no: 商户订单号
+ total_amount: 订单金额(分)
+ description: 商品描述
+ notify_url: 回调通知地址
+ openid: 用户openid (JSAPI支付必需)
+
+ Returns:
+ 返回支付参数,用于调起微信支付
+ """
+ timestamp = str(int(time.time()))
+ nonce_str = self._generate_nonce_str()
+
+ url = f"{self.api_base}/transactions/jsapi"
+
+ data = {
+ "appid": self.appid,
+ "mchid": self.mchid,
+ "description": description,
+ "out_trade_no": out_trade_no,
+ "time_expire": datetime.now().strftime("%Y-%m-%dT%H:%M:%S+08:00"),
+ "attach": "sqxp_order",
+ "notify_url": notify_url,
+ "amount": {
+ "total": total_amount,
+ "currency": "CNY"
+ },
+ "payer": {
+ "openid": openid
+ } if openid else None
+ }
+
+ # 移除None值
+ data = {k: v for k, v in data.items() if v is not None}
+
+ body = json.dumps(data)
+ sign = self._generate_sign_v3("POST", f"/v3/transactions/jsapi", timestamp, nonce_str, body)
+
+ headers = {
+ "Authorization": f'WECHATPAY2-SHA256-RSA2048 mchid="{self.mchid}",nonce_str="{nonce_str}",signature="{sign}",timestamp="{timestamp}",serial_no="{settings.WXPAY_SERIAL_NO}"',
+ "Content-Type": "application/json",
+ "Accept": "application/json"
+ }
+
+ try:
+ import ssl
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ if self.cert_path and self.private_key_path:
+ context.load_cert_chain(self.cert_path, self.private_key_path)
+
+ req = urllib.request.Request(url, data=body.encode(), headers=headers, method="POST")
+ with urllib.request.urlopen(req, context=context, timeout=30) as response:
+ result = json.loads(response.read().decode())
+
+ if "prepay_id" in result:
+ # 返回调起支付的参数
+ pay_sign = self._generate_pay_sign(result["prepay_id"])
+ return {
+ "success": True,
+ "prepay_id": result["prepay_id"],
+ "pay_sign": pay_sign,
+ "timestamp": timestamp,
+ "nonce_str": nonce_str
+ }
+ else:
+ return {"success": False, "error": result.get("message", "下单失败")}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ def _generate_pay_sign(self, prepay_id: str) -> str:
+ """生成支付签名 (小程序调起支付用)"""
+ timestamp = str(int(time.time()))
+ nonce_str = self._generate_nonce_str()
+
+ sign_str = f"{self.appid}\n{timestamp}\n{nonce_str}\nprepay_id={prepay_id}\n"
+ return hashlib.sha256(sign_str.encode('utf-8')).hexdigest()
+
+ def query_order(self, out_trade_no: str = None, transaction_id: str = None) -> Dict:
+ """查询订单"""
+ if not out_trade_no and not transaction_id:
+ return {"success": False, "error": "缺少订单号"}
+
+ timestamp = str(int(time.time()))
+ nonce_str = self._generate_nonce_str()
+
+ if out_trade_no:
+ url = f"{self.api_base}/v3/pay/transactions/out-trade-no/{out_trade_no}?mchid={self.mchid}"
+ else:
+ url = f"{self.api_base}/v3/pay/transactions/id/{transaction_id}?mchid={self.mchid}"
+
+ headers = {
+ "Authorization": f'WECHATPAY2-SHA256-RSA2048 mchid="{self.mchid}",nonce_str="{nonce_str}",signature="",timestamp="{timestamp}",serial_no=""',
+ "Accept": "application/json"
+ }
+
+ try:
+ import ssl
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ req = urllib.request.Request(url, headers=headers, method="GET")
+ with urllib.request.urlopen(req, context=context, timeout=30) as response:
+ result = json.loads(response.read().decode())
+ return {"success": True, "data": result}
+ except urllib.error.HTTPError as e:
+ return {"success": False, "error": f"HTTP {e.code}: {e.read().decode()}"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ def refund(
+ self,
+ out_trade_no: str,
+ total_amount: int,
+ refund_amount: int,
+ reason: str = None
+ ) -> Dict:
+ """
+ 申请退款
+
+ Args:
+ out_trade_no: 商户订单号
+ total_amount: 订单总金额(分)
+ refund_amount: 退款金额(分)
+ reason: 退款原因
+
+ Returns:
+ 退款结果
+ """
+ if refund_amount > total_amount:
+ return {"success": False, "error": "退款金额不能大于订单金额"}
+
+ timestamp = str(int(time.time()))
+ nonce_str = self._generate_nonce_str()
+ out_refund_no = f"REF{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}"
+
+ url = f"{self.api_base}/v3/refund/domestic/refunds"
+
+ data = {
+ "out_trade_no": out_trade_no,
+ "out_refund_no": out_refund_no,
+ "reason": reason or "用户申请退款",
+ "notify_url": settings.WXPAY_REFUND_NOTIFY_URL,
+ "amount": {
+ "refund": refund_amount,
+ "total": total_amount,
+ "currency": "CNY"
+ }
+ }
+
+ body = json.dumps(data)
+ sign = self._generate_sign_v3("POST", "/v3/refund/domestic/refunds", timestamp, nonce_str, body)
+
+ headers = {
+ "Authorization": f'WECHATPAY2-SHA256-RSA2048 mchid="{self.mchid}",nonce_str="{nonce_str}",signature="{sign}",timestamp="{timestamp}",serial_no="{settings.WXPAY_SERIAL_NO}"',
+ "Content-Type": "application/json",
+ "Accept": "application/json"
+ }
+
+ try:
+ import ssl
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ if self.cert_path and self.private_key_path:
+ context.load_cert_chain(self.cert_path, self.private_key_path)
+
+ req = urllib.request.Request(url, data=body.encode(), headers=headers, method="POST")
+ with urllib.request.urlopen(req, context=context, timeout=30) as response:
+ result = json.loads(response.read().decode())
+ return {
+ "success": True,
+ "refund_id": result.get("refund_id"),
+ "out_refund_no": out_refund_no,
+ "refund_status": result.get("status")
+ }
+ except urllib.error.HTTPError as e:
+ error_body = e.read().decode()
+ return {"success": False, "error": f"HTTP {e.code}: {error_body}"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ def close_order(self, out_trade_no: str) -> Dict:
+ """关闭订单"""
+ timestamp = str(int(time.time()))
+ nonce_str = self._generate_nonce_str()
+
+ url = f"{self.api_base}/v3/transactions/out-trade-no/{out_trade_no}/close"
+
+ data = {"mchid": self.mchid}
+ body = json.dumps(data)
+ sign = self._generate_sign_v3("POST", f"/v3/transactions/out-trade-no/{out_trade_no}/close", timestamp, nonce_str, body)
+
+ headers = {
+ "Authorization": f'WECHATPAY2-SHA256-RSA2048 mchid="{self.mchid}",nonce_str="{nonce_str}",signature="{sign}",timestamp="{timestamp}",serial_no="{settings.WXPAY_SERIAL_NO}"',
+ "Content-Type": "application/json"
+ }
+
+ try:
+ import ssl
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ if self.cert_path and self.private_key_path:
+ context.load_cert_chain(self.cert_path, self.private_key_path)
+
+ req = urllib.request.Request(url, data=body.encode(), headers=headers, method="POST")
+ with urllib.request.urlopen(req, context=context, timeout=30) as response:
+ return {"success": True}
+ except urllib.error.HTTPError as e:
+ return {"success": False, "error": f"HTTP {e.code}: {e.read().decode()}"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+
+def get_wechat_pay_client() -> WechatPayClient:
+ """获取微信支付客户端实例"""
+ return WechatPayClient()
+
+
+# 全局实例 (延迟初始化)
+_wechat_pay_client: Optional[WechatPayClient] = None
+
+
+def wechat_pay() -> WechatPayClient:
+ """获取微信支付客户端"""
+ global _wechat_pay_client
+ if _wechat_pay_client is None:
+ _wechat_pay_client = WechatPayClient()
+ return _wechat_pay_client
+
+
+class WechatPayV2Client:
+ """微信支付 V2 客户端 (兼容旧版)"""
+
+ def __init__(self):
+ self.mchid = settings.WXPAY_MCHID
+ self.appid = settings.WXPAY_APPID
+ self.api_key = settings.WXPAY_API_KEY
+ self.appid_mini = settings.WXPAY_MINI_APPID
+ self.notify_url = settings.WXPAY_NOTIFY_URL
+
+ if settings.WXPAY_SANDBOX:
+ self.pay_url = "https://sandbox.mch.weixin.qq.com/pay/unifiedorder"
+ else:
+ self.pay_url = "https://api.mch.weixin.qq.com/pay/unifiedorder"
+
+ def _generate_sign(self, params: Dict) -> str:
+ """生成签名"""
+ sorted_keys = sorted(params.keys())
+ sign_str = "&".join([f"{k}={params[k]}" for k in sorted_keys if params[k]])
+ sign_str += f"&key={self.api_key}"
+ return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
+
+ def _dict_to_xml(self, params: Dict) -> str:
+ """字典转XML"""
+ xml_str = ""
+ for k, v in params.items():
+ xml_str += f"<{k}>{k}>"
+ xml_str += ""
+ return xml_str
+
+ def _xml_to_dict(self, xml_str: str) -> Dict:
+ """XML转字典"""
+ try:
+ root = ET.fromstring(xml_str)
+ return {child.tag: child.text for child in root}
+ except:
+ return {}
+
+ def build_miniprogram_pay_params(self, prepay_id: str) -> Dict[str, str]:
+ """小程序调起支付的二次签名参数(与公众号 JSAPI 规则一致)"""
+ appid = self.appid_mini or self.appid
+ time_stamp = str(int(time.time()))
+ nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+ package = f"prepay_id={prepay_id}"
+ sign_type = "MD5"
+ pay_fields = {
+ "appId": appid,
+ "nonceStr": nonce_str,
+ "package": package,
+ "signType": sign_type,
+ "timeStamp": time_stamp,
+ }
+ sign_str = "&".join([f"{k}={pay_fields[k]}" for k in sorted(pay_fields.keys())])
+ sign_str += f"&key={self.api_key}"
+ pay_sign = hashlib.md5(sign_str.encode("utf-8")).hexdigest().upper()
+ return {
+ "appId": appid,
+ "timeStamp": time_stamp,
+ "nonceStr": nonce_str,
+ "package": package,
+ "signType": sign_type,
+ "paySign": pay_sign,
+ }
+
+ def unified_order(
+ self,
+ out_trade_no: str,
+ total_fee: int,
+ body: str,
+ openid: str = None,
+ trade_type: str = "JSAPI"
+ ) -> Dict:
+ """
+ 统一下单 (V2 API)
+
+ Args:
+ out_trade_no: 商户订单号
+ total_fee: 订单金额(分)
+ body: 商品描述
+ openid: 用户openid
+ trade_type: 交易类型 JSAPI/NATIVE/APP
+
+ Returns:
+ 返回支付参数
+ """
+ params = {
+ "appid": self.appid,
+ "mch_id": self.mchid,
+ "nonce_str": ''.join(random.choices(string.ascii_letters + string.digits, k=32)),
+ "body": body,
+ "out_trade_no": out_trade_no,
+ "total_fee": total_fee,
+ "spbill_create_ip": "127.0.0.1",
+ "notify_url": self.notify_url,
+ "trade_type": trade_type
+ }
+
+ if openid:
+ params["openid"] = openid
+
+ # 小程序 JSAPI 必须使用小程序 AppID 与商户号绑定关系一致
+ if trade_type == "JSAPI" and self.appid_mini:
+ params["appid"] = self.appid_mini
+
+ # 生成签名
+ params["sign"] = self._generate_sign(params)
+
+ # 发送请求
+ try:
+ import ssl
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ req = urllib.request.Request(
+ self.pay_url,
+ data=self._dict_to_xml(params).encode('utf-8'),
+ headers={"Content-Type": "text/xml"}
+ )
+
+ with urllib.request.urlopen(req, context=context, timeout=30) as response:
+ result = self._xml_to_dict(response.read().decode())
+
+ if result.get("return_code") == "SUCCESS" and result.get("result_code") == "SUCCESS":
+ if trade_type == "JSAPI":
+ prepay = result.get("prepay_id")
+ pay_extra = self.build_miniprogram_pay_params(prepay) if prepay else {}
+ return {
+ "success": True,
+ "prepay_id": prepay,
+ "package": pay_extra.get("package"),
+ "pay_params": pay_extra,
+ }
+ elif trade_type == "NATIVE":
+ return {
+ "success": True,
+ "code_url": result.get("code_url")
+ }
+ elif trade_type == "APP":
+ return {
+ "success": True,
+ "prepay_id": result.get("prepay_id")
+ }
+ else:
+ return {
+ "success": False,
+ "error": result.get("err_code_des", result.get("return_msg", "下单失败"))
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ def query_order(self, out_trade_no: str = None, transaction_id: str = None) -> Dict:
+ """查询订单"""
+ if not out_trade_no and not transaction_id:
+ return {"success": False, "error": "缺少订单号"}
+
+ params = {
+ "appid": self.appid,
+ "mch_id": self.mchid,
+ "nonce_str": ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+ }
+
+ if out_trade_no:
+ params["out_trade_no"] = out_trade_no
+ else:
+ params["transaction_id"] = transaction_id
+
+ params["sign"] = self._generate_sign(params)
+
+ query_url = "https://api.mch.weixin.qq.com/pay/orderquery"
+
+ try:
+ import ssl
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ req = urllib.request.Request(
+ query_url,
+ data=self._dict_to_xml(params).encode('utf-8'),
+ headers={"Content-Type": "text/xml"}
+ )
+
+ with urllib.request.urlopen(req, context=context, timeout=30) as response:
+ result = self._xml_to_dict(response.read().decode())
+ return {"success": True, "data": result}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ def refund(
+ self,
+ out_trade_no: str,
+ total_fee: int,
+ refund_fee: int,
+ refund_desc: str = None
+ ) -> Dict:
+ """申请退款 (V2)"""
+ out_refund_no = f"REF{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}"
+
+ params = {
+ "appid": self.appid,
+ "mch_id": self.mchid,
+ "nonce_str": ''.join(random.choices(string.ascii_letters + string.digits, k=32)),
+ "out_trade_no": out_trade_no,
+ "out_refund_no": out_refund_no,
+ "total_fee": total_fee,
+ "refund_fee": refund_fee,
+ "refund_desc": refund_desc or "用户申请退款"
+ }
+
+ params["sign"] = self._generate_sign(params)
+
+ refund_url = "https://api.mch.weixin.qq.com/secapi/pay/refund"
+
+ try:
+ import ssl
+ context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ # 需要双向证书
+ if settings.WXPAY_SSL_CERT and settings.WXPAY_SSL_KEY:
+ cert_file = settings.WXPAY_SSL_CERT
+ key_file = settings.WXPAY_SSL_KEY
+ req = urllib.request.Request(
+ refund_url,
+ data=self._dict_to_xml(params).encode('utf-8'),
+ headers={"Content-Type": "text/xml"}
+ )
+ with urllib.request.urlopen(req, context=context, timeout=30) as response:
+ result = self._xml_to_dict(response.read().decode())
+ if result.get("return_code") == "SUCCESS":
+ return {
+ "success": True,
+ "out_refund_no": out_refund_no,
+ "refund_id": result.get("refund_id")
+ }
+ return {"success": False, "error": result.get("return_msg", "退款失败")}
+ else:
+ return {"success": False, "error": "缺少SSL证书配置"}
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ def parse_notify(self, xml_data: str) -> Dict:
+ """解析支付回调"""
+ result = self._xml_to_dict(xml_data)
+ if result.get("return_code") == "SUCCESS":
+ # 验证签名
+ sign = result.pop("sign", None)
+ if sign and sign == self._generate_sign(result):
+ return {"success": True, "data": result}
+ return {"success": False, "error": "签名验证失败"}
+
+
+def get_wechat_pay_v2() -> WechatPayV2Client:
+ """获取微信支付V2客户端"""
+ return WechatPayV2Client()
diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py
new file mode 100644
index 00000000..ceb89e4b
--- /dev/null
+++ b/backend/app/crud/__init__.py
@@ -0,0 +1,73 @@
+"""CRUD模块"""
+from app.crud.base import CRUDBase, HistoryCRUD
+from app.crud.region import region_crud
+from app.crud.category import category_crud
+from app.crud.unit import unit_crud
+from app.crud.user import user_crud
+from app.crud.password_history import password_history_crud
+from app.crud.product import product_crud
+from app.crud.address import address_crud
+from app.crud.inquiry import inquiry_crud, inquiry_item_crud
+from app.crud.quotation import quotation_crud, quotation_item_crud
+from app.crud.price_list import price_list_crud, price_list_item_crud
+from app.crud.order import order_crud, order_item_crud, order_status_log_crud
+from app.crud.delivery import delivery_task_crud, delivery_task_item_crud
+from app.crud.settlement import settlement_crud, refund_crud
+from app.crud.ord_settlement import (
+ ord_client_settlement_crud,
+ ord_client_settlement_item_crud,
+ ord_supplier_settlement_crud,
+ ord_supplier_settlement_item_crud,
+)
+from app.crud.deliverer_review import deliverer_review_crud
+from app.crud.product_banner import product_banner, product_banner_his
+from app.crud.product_detail_media import product_detail_media, product_detail_media_his
+from app.crud.product_parameter import product_parameter, product_parameter_his
+from app.crud.category_parameter import category_parameter, category_parameter_his
+from app.crud.product_parameter_value import product_parameter_value, product_parameter_value_his
+from app.crud.product_review import product_review, product_review_his
+from app.crud.product_review_media import product_review_media, product_review_media_his
+
+__all__ = [
+ "CRUDBase",
+ "HistoryCRUD",
+ "region_crud",
+ "category_crud",
+ "unit_crud",
+ "user_crud",
+ "password_history_crud",
+ "product_crud",
+ "address_crud",
+ "inquiry_crud",
+ "inquiry_item_crud",
+ "quotation_crud",
+ "quotation_item_crud",
+ "price_list_crud",
+ "price_list_item_crud",
+ "order_crud",
+ "order_item_crud",
+ "order_status_log_crud",
+ "delivery_task_crud",
+ "delivery_task_item_crud",
+ "settlement_crud",
+ "refund_crud",
+ "ord_client_settlement_crud",
+ "ord_client_settlement_item_crud",
+ "ord_supplier_settlement_crud",
+ "ord_supplier_settlement_item_crud",
+ "deliverer_review_crud",
+ "product_banner",
+ "product_banner_his",
+ "product_detail_media",
+ "product_detail_media_his",
+ "product_parameter",
+ "product_parameter_his",
+ "category_parameter",
+ "category_parameter_his",
+ "product_parameter_value",
+ "product_parameter_value_his",
+ "product_review",
+ "product_review_his",
+ "product_review_media",
+ "product_review_media_his",
+]
diff --git a/backend/app/crud/address.py b/backend/app/crud/address.py
new file mode 100644
index 00000000..8c4241e3
--- /dev/null
+++ b/backend/app/crud/address.py
@@ -0,0 +1,122 @@
+"""地址CRUD"""
+from typing import Optional, List, Tuple
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from app.models.models import BasAddress, BasAddressHis
+from app.crud.base import CRUDBase
+
+
+class AddressCRUD(CRUDBase[BasAddress]):
+ """地址CRUD操作"""
+
+ def get_by_user(self, db: Session, user_id: str) -> List[BasAddress]:
+ """获取用户的所有地址"""
+ return db.query(BasAddress).filter(
+ and_(BasAddress.user_id == user_id, BasAddress.is_del == 0)
+ ).order_by(BasAddress.create_at.desc(), BasAddress.id.desc()).all()
+
+ def get_by_user_paged(
+ self, db: Session, user_id: str, skip: int = 0, limit: int = 20
+ ) -> Tuple[List[BasAddress], int]:
+ """分页获取用户地址(按创建时间倒序)"""
+ q = db.query(BasAddress).filter(
+ and_(BasAddress.user_id == user_id, BasAddress.is_del == 0)
+ )
+ total = q.count()
+ rows = (
+ q.order_by(BasAddress.create_at.desc(), BasAddress.id.desc())
+ .offset(skip)
+ .limit(limit)
+ .all()
+ )
+ return rows, total
+
+ def get_by_user_and_type(self, db: Session, user_id: str, address_type: str) -> List[BasAddress]:
+ """获取用户指定类型的地址"""
+ return db.query(BasAddress).filter(
+ and_(BasAddress.user_id == user_id, BasAddress.address_type == address_type, BasAddress.is_del == 0)
+ ).order_by(BasAddress.create_at.desc(), BasAddress.id.desc()).all()
+
+ def get_default(self, db: Session, user_id: str, address_type: str = None) -> Optional[BasAddress]:
+ """获取用户默认地址"""
+ query = db.query(BasAddress).filter(
+ and_(BasAddress.user_id == user_id, BasAddress.is_default == True, BasAddress.is_del == 0)
+ )
+ if address_type:
+ query = query.filter(BasAddress.address_type == address_type)
+ return query.first()
+
+ def set_default(self, db: Session, user_id: str, address_id: str) -> bool:
+ """设置默认地址"""
+ # 先取消所有默认地址
+ db.query(BasAddress).filter(
+ and_(BasAddress.user_id == user_id, BasAddress.is_default == True)
+ ).update({"is_default": False})
+
+ # 设置新的默认地址
+ address = self.get(db, address_id)
+ if address and address.user_id == user_id:
+ address.is_default = True
+ db.commit()
+ return True
+ return False
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BasAddress]:
+ """更新地址并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "user_id": original.user_id,
+ "address_type": original.address_type,
+ "contact_person": original.contact_person,
+ "contact_phone": original.contact_phone,
+ "province": original.province,
+ "city": original.city,
+ "district": original.district,
+ "detail_address": original.detail_address,
+ "is_default": original.is_default,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BasAddressHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, id: str) -> bool:
+ """删除地址并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return False
+
+ his_data = {
+ "id": original.id,
+ "user_id": original.user_id,
+ "address_type": original.address_type,
+ "contact_person": original.contact_person,
+ "contact_phone": original.contact_phone,
+ "province": original.province,
+ "city": original.city,
+ "district": original.district,
+ "detail_address": original.detail_address,
+ "is_default": original.is_default,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BasAddressHis(**his_data)
+ db.add(his)
+
+ return self.delete(db, id)
+
+
+address_crud = AddressCRUD(BasAddress)
diff --git a/backend/app/crud/api_request_log.py b/backend/app/crud/api_request_log.py
new file mode 100644
index 00000000..28413eb4
--- /dev/null
+++ b/backend/app/crud/api_request_log.py
@@ -0,0 +1,122 @@
+"""API 请求日志 CRUD"""
+from datetime import datetime
+from typing import List, Optional, Tuple
+
+from sqlalchemy import and_, desc, func, or_
+from sqlalchemy.orm import Session
+
+from app.core.utils import generate_id
+from app.models.models import SysApiRequestLog, SysApiRequestLogHis
+
+
+class ApiRequestLogCRUD:
+ @staticmethod
+ def get_multi_with_filters(
+ db: Session,
+ http_method: str = None,
+ status_code: int = None,
+ keyword: str = None,
+ path_prefix: str = None,
+ start_date: str = None,
+ end_date: str = None,
+ skip: int = 0,
+ limit: int = 10,
+ ) -> Tuple[List[SysApiRequestLog], int]:
+ q = db.query(SysApiRequestLog).filter(SysApiRequestLog.is_del == 0)
+ if http_method:
+ q = q.filter(SysApiRequestLog.http_method == http_method.upper())
+ if status_code is not None:
+ q = q.filter(SysApiRequestLog.status_code == status_code)
+ if path_prefix:
+ q = q.filter(SysApiRequestLog.path.like(f"%{path_prefix}%"))
+ if keyword:
+ kw = f"%{keyword}%"
+ q = q.filter(
+ or_(
+ SysApiRequestLog.path.like(kw),
+ SysApiRequestLog.username.like(kw),
+ SysApiRequestLog.user_id.like(kw),
+ SysApiRequestLog.request_body.like(kw),
+ SysApiRequestLog.response_body.like(kw),
+ )
+ )
+ if start_date:
+ try:
+ start_dt = datetime.strptime(start_date, "%Y-%m-%d")
+ q = q.filter(SysApiRequestLog.create_at >= start_dt)
+ except Exception:
+ pass
+ if end_date:
+ try:
+ end_dt = datetime.strptime(end_date, "%Y-%m-%d")
+ end_dt = end_dt.replace(hour=23, minute=59, second=59)
+ q = q.filter(SysApiRequestLog.create_at <= end_dt)
+ except Exception:
+ pass
+
+ total = int(
+ q.with_entities(func.count(SysApiRequestLog.id)).order_by(None).scalar() or 0
+ )
+ rows = (
+ q.with_entities(
+ SysApiRequestLog.id,
+ SysApiRequestLog.http_method,
+ SysApiRequestLog.path,
+ SysApiRequestLog.query_string,
+ SysApiRequestLog.status_code,
+ SysApiRequestLog.duration_ms,
+ SysApiRequestLog.user_id,
+ SysApiRequestLog.username,
+ SysApiRequestLog.role,
+ SysApiRequestLog.ip_address,
+ SysApiRequestLog.user_agent,
+ func.substr(SysApiRequestLog.request_body, 1, 300).label("request_body"),
+ func.substr(SysApiRequestLog.response_body, 1, 300).label("response_body"),
+ SysApiRequestLog.create_at,
+ )
+ .order_by(desc(SysApiRequestLog.create_at))
+ .offset(skip)
+ .limit(limit)
+ .all()
+ )
+ return rows, total
+
+ @staticmethod
+ def get(db: Session, log_id: str) -> Optional[SysApiRequestLog]:
+ return db.query(SysApiRequestLog).filter(
+ and_(SysApiRequestLog.id == log_id, SysApiRequestLog.is_del == 0)
+ ).first()
+
+ @staticmethod
+ def delete(db: Session, log_id: str) -> bool:
+ log = db.query(SysApiRequestLog).filter(SysApiRequestLog.id == log_id).first()
+ if not log:
+ return False
+ his = SysApiRequestLogHis(
+ id=log.id,
+ http_method=log.http_method,
+ path=log.path,
+ query_string=log.query_string,
+ request_body=log.request_body,
+ response_body=log.response_body,
+ status_code=log.status_code,
+ duration_ms=log.duration_ms,
+ user_id=log.user_id,
+ username=log.username,
+ role=log.role,
+ ip_address=log.ip_address,
+ user_agent=log.user_agent,
+ create_at=log.create_at,
+ create_by=log.create_by,
+ create_name=log.create_name,
+ update_at=log.update_at,
+ update_by=log.update_by,
+ his_create_at=datetime.now(),
+ )
+ db.add(his)
+ log.is_del = 1
+ db.commit()
+ return True
+
+
+api_request_log_crud = ApiRequestLogCRUD()
diff --git a/backend/app/crud/attachment.py b/backend/app/crud/attachment.py
new file mode 100644
index 00000000..5460f4b0
--- /dev/null
+++ b/backend/app/crud/attachment.py
@@ -0,0 +1,58 @@
+"""附件CRUD"""
+from typing import Optional, List
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from app.models.models import BizAttachment
+from app.crud.base import CRUDBase
+from app.core.utils import generate_id
+
+
+class AttachmentCRUD(CRUDBase[BizAttachment]):
+ """附件CRUD操作"""
+
+ def get_by_business(
+ self,
+ db: Session,
+ business_type: str,
+ business_id: str
+ ) -> List[BizAttachment]:
+ """根据业务类型和业务ID获取附件列表"""
+ return db.query(self.model).filter(
+ and_(
+ self.model.business_type == business_type,
+ self.model.business_id == business_id,
+ self.model.is_del == 0
+ )
+ ).order_by(self.model.create_at.desc()).all()
+
+ def create_attachment(
+ self,
+ db: Session,
+ business_type: str,
+ business_id: str,
+ file_name: str,
+ file_url: str,
+ file_size: int = None,
+ file_type: str = None,
+ upload_by: str = None,
+ upload_by_name: str = None
+ ) -> BizAttachment:
+ """创建附件记录"""
+ data = {
+ "id": generate_id(),
+ "business_type": business_type,
+ "business_id": business_id,
+ "file_name": file_name,
+ "file_url": file_url,
+ "file_size": file_size,
+ "file_type": file_type,
+ "upload_by": upload_by,
+ "upload_by_name": upload_by_name,
+ "create_by": upload_by,
+ "create_name": upload_by_name
+ }
+ return self.create(db, data)
+
+
+attachment_crud = AttachmentCRUD(BizAttachment)
diff --git a/backend/app/crud/base.py b/backend/app/crud/base.py
new file mode 100644
index 00000000..5fa3e208
--- /dev/null
+++ b/backend/app/crud/base.py
@@ -0,0 +1,166 @@
+"""CRUD基础类"""
+from typing import TypeVar, Generic, Type, Optional, List, Any
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, or_
+import re
+
+from app.core.utils import generate_id
+from app.core.database import Base
+from sqlalchemy import func
+
+ModelType = TypeVar("ModelType", bound=Base)
+
+
+def camel_to_snake(name: str) -> str:
+ """驼峰命名转蛇形命名"""
+ s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
+ return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
+
+
+def get_model_attr(obj, key: str):
+ """获取模型的属性名,支持驼峰转蛇形"""
+ # 先尝试直接匹配
+ if hasattr(obj, key):
+ return key
+ # 尝试驼峰转蛇形
+ snake_key = camel_to_snake(key)
+ if hasattr(obj, snake_key):
+ return snake_key
+ # 如果都没有,返回None
+ return None
+
+
+class CRUDBase(Generic[ModelType]):
+ """CRUD基类"""
+
+ def __init__(self, model: Type[ModelType]):
+ self.model = model
+
+ def get(self, db: Session, id: str) -> Optional[ModelType]:
+ """根据ID获取单条记录"""
+ return db.query(self.model).filter(self.model.id == id).first()
+
+ def get_multi(
+ self,
+ db: Session,
+ skip: int = 0,
+ limit: int = 100,
+ filters: List[Any] = None,
+ order_by: str = None,
+ order_type: str = "desc"
+ ) -> List[ModelType]:
+ """获取多条记录"""
+ query = db.query(self.model)
+ if filters:
+ query = query.filter(*filters)
+ if order_by:
+ order_column = getattr(self.model, order_by, None)
+ if order_column:
+ if order_type == "asc":
+ query = query.order_by(order_column.asc())
+ else:
+ query = query.order_by(order_column.desc())
+ else:
+ query = query.order_by(self.model.create_at.desc())
+ return query.offset(skip).limit(limit).all()
+
+ def get_all(self, db: Session, filters: List[Any] = None) -> List[ModelType]:
+ """获取所有记录"""
+ query = db.query(self.model)
+ if filters:
+ query = query.filter(*filters)
+ return query.all()
+
+ def create(self, db: Session, obj_in: dict, creator: dict = None) -> ModelType:
+ """创建记录"""
+ obj = self.model()
+ for key, value in obj_in.items():
+ attr_name = get_model_attr(obj, key)
+ if attr_name:
+ setattr(obj, attr_name, value)
+ # 设置ID
+ if not obj.id:
+ obj.id = generate_id()
+ # 设置创建人信息
+ if creator:
+ if not obj.create_by and "user_id" in creator:
+ obj.create_by = creator["user_id"]
+ if not obj.create_name and "real_name" in creator:
+ obj.create_name = creator["real_name"]
+ db.add(obj)
+ db.commit()
+ db.refresh(obj)
+ return obj
+
+ def update(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[ModelType]:
+ """更新记录"""
+ obj = self.get(db, id)
+ if obj:
+ for key, value in obj_in.items():
+ attr_name = get_model_attr(obj, key)
+ if attr_name and key not in ["id", "create_at", "create_by", "create_name"]:
+ setattr(obj, attr_name, value)
+ # 设置更新人信息
+ if updater:
+ if "user_id" in updater:
+ obj.update_by = updater["user_id"]
+ if "real_name" in updater:
+ obj.update_name = updater.get("real_name", obj.create_name)
+ obj.update_at = func.now()
+ db.commit()
+ db.refresh(obj)
+ return obj
+
+ def delete(self, db: Session, id: str) -> bool:
+ """删除记录(软删除)"""
+ obj = self.get(db, id)
+ if obj:
+ if hasattr(obj, "is_del"):
+ obj.is_del = 1
+ else:
+ db.delete(obj)
+ db.commit()
+ return True
+ return False
+
+ def hard_delete(self, db: Session, id: str) -> bool:
+ """物理删除"""
+ obj = self.get(db, id)
+ if obj:
+ db.delete(obj)
+ db.commit()
+ return True
+ return False
+
+ def count(self, db: Session, filters: List[Any] = None) -> int:
+ """统计数量"""
+ query = db.query(self.model)
+ if filters:
+ query = query.filter(*filters)
+ return query.count()
+
+ def exists(self, db: Session, filters: List[Any] = None) -> bool:
+ """检查记录是否存在"""
+ query = db.query(self.model)
+ if filters:
+ query = query.filter(*filters)
+ return query.first() is not None
+
+
+class HistoryCRUD(CRUDBase[ModelType]):
+ """历史表CRUD类"""
+
+ def create_history(self, db: Session, original_data: dict) -> ModelType:
+ """创建历史记录"""
+ obj = self.model()
+ # 复制原始数据到历史表
+ for key, value in original_data.items():
+ if hasattr(obj, key):
+ setattr(obj, key, value)
+ # 设置历史记录创建时间
+ if hasattr(obj, "his_create_at"):
+ obj.his_create_at = func.now()
+ db.add(obj)
+ db.commit()
+ db.refresh(obj)
+ return obj
diff --git a/backend/app/crud/category.py b/backend/app/crud/category.py
new file mode 100644
index 00000000..8abfda7e
--- /dev/null
+++ b/backend/app/crud/category.py
@@ -0,0 +1,112 @@
+"""商品分类CRUD"""
+from typing import Optional, List
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from app.models.models import BasProductCategory, BasProductCategoryHis
+from app.crud.base import CRUDBase
+
+
+class CategoryCRUD(CRUDBase[BasProductCategory]):
+ """商品分类CRUD操作"""
+
+ def get_by_name(self, db: Session, name: str, parent_id: str = None) -> Optional[BasProductCategory]:
+ """根据名称获取分类"""
+ query = db.query(BasProductCategory).filter(
+ and_(BasProductCategory.name == name, BasProductCategory.is_del == 0)
+ )
+ if parent_id:
+ query = query.filter(BasProductCategory.parent_id == parent_id)
+ else:
+ query = query.filter(BasProductCategory.parent_id.is_(None))
+ return query.first()
+
+ def get_active_all(self, db: Session) -> List[BasProductCategory]:
+ """获取所有启用的分类"""
+ return db.query(BasProductCategory).filter(
+ and_(BasProductCategory.is_del == 0, BasProductCategory.is_active == True)
+ ).order_by(BasProductCategory.sort_order.asc()).all()
+
+ def get_children(self, db: Session, parent_id: str) -> List[BasProductCategory]:
+ """获取子分类"""
+ return db.query(BasProductCategory).filter(
+ and_(BasProductCategory.parent_id == parent_id, BasProductCategory.is_del == 0)
+ ).order_by(BasProductCategory.sort_order.asc()).all()
+
+ def get_tree(self, db: Session) -> List[dict]:
+ """获取分类树"""
+ all_categories = self.get_active_all(db)
+ return self._build_tree(all_categories, None)
+
+ def _build_tree(self, categories: List[BasProductCategory], parent_id: str = None) -> List[dict]:
+ """构建树形结构"""
+ tree = []
+ for cat in categories:
+ if cat.parent_id == parent_id:
+ pc = getattr(cat, "price_coefficient", None)
+ node = {
+ "id": cat.id,
+ "name": cat.name,
+ "parentId": cat.parent_id,
+ "sortOrder": cat.sort_order,
+ "priceCoefficient": float(pc) if pc is not None else 0.0,
+ "isActive": cat.is_active,
+ "children": self._build_tree(categories, cat.id)
+ }
+ tree.append(node)
+ return tree
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BasProductCategory]:
+ """更新分类并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ pc_hist = getattr(original, "price_coefficient", None)
+ his_data = {
+ "id": original.id,
+ "name": original.name,
+ "image_url": getattr(original, "image_url", None),
+ "parent_id": original.parent_id,
+ "sort_order": original.sort_order,
+ "price_coefficient": pc_hist if pc_hist is not None else 0,
+ "is_active": original.is_active,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BasProductCategoryHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, id: str) -> bool:
+ """删除分类并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return False
+
+ pc_hist = getattr(original, "price_coefficient", None)
+ his_data = {
+ "id": original.id,
+ "name": original.name,
+ "image_url": getattr(original, "image_url", None),
+ "parent_id": original.parent_id,
+ "sort_order": original.sort_order,
+ "price_coefficient": pc_hist if pc_hist is not None else 0,
+ "is_active": original.is_active,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BasProductCategoryHis(**his_data)
+ db.add(his)
+
+ return self.delete(db, id)
+
+
+category_crud = CategoryCRUD(BasProductCategory)
diff --git a/backend/app/crud/category_parameter.py b/backend/app/crud/category_parameter.py
new file mode 100644
index 00000000..2f0afb93
--- /dev/null
+++ b/backend/app/crud/category_parameter.py
@@ -0,0 +1,116 @@
+"""CRUD - 分类参数关联"""
+from typing import List
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from app.crud.base import CRUDBase, HistoryCRUD
+from app.models.category_parameter import BasCategoryParameter, BasCategoryParameterHis
+from app.schemas.category_parameter import CategoryParameterBase
+
+
+class CategoryParameterCRUD(CRUDBase[BasCategoryParameter]):
+ """分类参数关联CRUD操作"""
+
+ def get_by_category(self, db: Session, category_id: str) -> List[BasCategoryParameter]:
+ """获取分类的所有关联参数"""
+ return db.query(self.model).filter(
+ self.model.category_id == category_id,
+ self.model.is_del == 0
+ ).order_by(self.model.sort_order.asc()).all()
+
+ def get_by_category_with_detail(self, db: Session, category_id: str) -> List[dict]:
+ """获取分类的所有关联参数(带参数详情)"""
+ from app.models.product_parameter import BasProductParameter
+
+ results = db.query(
+ BasCategoryParameter,
+ BasProductParameter
+ ).join(
+ BasProductParameter,
+ and_(
+ BasCategoryParameter.parameter_id == BasProductParameter.id,
+ BasProductParameter.is_del == 0
+ )
+ ).filter(
+ BasCategoryParameter.category_id == category_id,
+ BasCategoryParameter.is_del == 0
+ ).order_by(
+ BasCategoryParameter.sort_order.asc()
+ ).all()
+
+ return [
+ {
+ "id": cp.id,
+ "parameterId": cp.parameter_id,
+ "parameterName": pp.name,
+ "parameterCode": pp.code,
+ "paramType": pp.param_type,
+ "options": pp.options,
+ "placeholder": pp.placeholder,
+ "isRequired": cp.is_required,
+ "defaultValue": cp.default_value,
+ "sortOrder": cp.sort_order,
+ }
+ for cp, pp in results
+ ]
+
+ def batch_save(
+ self,
+ db: Session,
+ category_id: str,
+ items: List[dict],
+ creator: dict = None
+ ) -> List[BasCategoryParameter]:
+ """批量保存分类参数关联"""
+ # 先获取要删除的记录ID
+ old_records = db.query(self.model).filter(
+ self.model.category_id == category_id,
+ self.model.is_del == 0
+ ).all()
+
+ # 物理删除旧记录(释放唯一索引约束)
+ for record in old_records:
+ db.delete(record)
+ db.commit()
+
+ # 创建新的关联
+ saved_items = []
+ for idx, item in enumerate(items):
+ item_data = {
+ "category_id": category_id,
+ "parameter_id": item.get("parameter_id"),
+ "is_required": item.get("is_required", 0),
+ "default_value": item.get("default_value"),
+ "sort_order": item.get("sort_order", idx),
+ }
+ cp = self.create(db, item_data, creator)
+ saved_items.append(cp)
+
+ db.commit()
+ return saved_items
+
+ def delete_by_category(self, db: Session, category_id: str) -> int:
+ """删除分类的所有关联"""
+ count = db.query(self.model).filter(
+ self.model.category_id == category_id,
+ self.model.is_del == 0
+ ).update({"is_del": 1})
+ db.commit()
+ return count
+
+ def exists_link(self, db: Session, category_id: str, parameter_id: str) -> bool:
+ """检查是否已有关联"""
+ return db.query(self.model).filter(
+ self.model.category_id == category_id,
+ self.model.parameter_id == parameter_id,
+ self.model.is_del == 0
+ ).first() is not None
+
+
+class CategoryParameterHisCRUD(HistoryCRUD[BasCategoryParameterHis]):
+ """分类参数关联历史CRUD操作"""
+ pass
+
+
+category_parameter = CategoryParameterCRUD(BasCategoryParameter)
+category_parameter_his = CategoryParameterHisCRUD(BasCategoryParameterHis)
diff --git a/backend/app/crud/client_cart.py b/backend/app/crud/client_cart.py
new file mode 100644
index 00000000..db69ebf1
--- /dev/null
+++ b/backend/app/crud/client_cart.py
@@ -0,0 +1,280 @@
+"""客户购物车 CRUD"""
+from decimal import Decimal
+from typing import List, Optional, Dict, Any
+from collections import defaultdict
+from datetime import datetime
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from app.models.models import BizClientCart, BizClientCartHis
+from app.core.utils import generate_id
+from app.crud.price_list import price_list_crud, price_list_item_crud
+from app.crud.unit import unit_crud
+from app.crud.product import product_crud
+from app.crud.category import category_crud
+
+
+class ClientCartCRUD:
+ def list_by_client(self, db: Session, client_id: str) -> List[BizClientCart]:
+ return (
+ db.query(BizClientCart)
+ .filter(and_(BizClientCart.client_id == client_id, BizClientCart.is_del == 0))
+ .order_by(BizClientCart.create_at.desc())
+ .all()
+ )
+
+ def sync_to_active_price_list(
+ self,
+ db: Session,
+ client_id: str,
+ region_id: str,
+ operator: dict,
+ rows: Optional[List[BizClientCart]] = None,
+ ) -> bool:
+ """
+ 将购物车中仍属于当前生效价格清单的商品行,同步为区域最新生效清单的单价、单位与清单 ID。
+ 同一商品多条行(历史清单 ID 不同)合并数量并保留一行。
+ 无生效清单时返回 False,不修改数据。
+ """
+ pl = price_list_crud.get_active(db, region_id)
+ if not pl:
+ return False
+
+ rows = rows if rows is not None else self.list_by_client(db, client_id)
+ if not rows:
+ return True
+
+ # 快速路径:如果购物车所有行已经属于当前生效价格清单,则无需做任何同步。
+ # 这能避免在“频繁加购/改数量”场景下对整车做 update + his insert,导致卡顿。
+ if all(getattr(r, "price_list_id", None) == pl.id for r in rows):
+ return True
+
+ price_items = price_list_item_crud.get_by_price_list(db, pl.id)
+ pmap: Dict[str, Any] = {i.product_id: i for i in price_items}
+
+ by_product: Dict[str, List[BizClientCart]] = defaultdict(list)
+ for r in rows:
+ if r.product_id in pmap:
+ by_product[r.product_id].append(r)
+
+ op_uid = operator.get("user_id")
+ now = datetime.now()
+
+ changed = False
+
+ for product_id, rel in by_product.items():
+ pi = pmap[product_id]
+ total_qty = sum(Decimal(str(r.quantity)) for r in rel)
+ if total_qty <= 0:
+ continue
+ new_unit_price = Decimal(str(pi.price))
+ new_amount = (total_qty * new_unit_price).quantize(Decimal("0.01"))
+ keep = next((r for r in rel if r.price_list_id == pl.id), rel[0])
+
+ for r in rel:
+ if r.id != keep.id:
+ self._copy_to_his(db, r)
+ r.is_del = 1
+ r.update_by = op_uid
+ r.update_at = now
+ changed = True
+
+ # 只有发生实际变化时才写历史并更新主表,避免“重复同步”导致的历史膨胀与写放大
+ product = product_crud.get(db, product_id)
+ unit = unit_crud.get(db, pi.unit_id) if pi.unit_id else None
+ cat_name = keep.category_name or ""
+ if product and product.category_id:
+ cat = category_crud.get(db, product.category_id)
+ cat_name = cat.name if cat else cat_name
+
+ need_update = (
+ keep.price_list_id != pl.id
+ or keep.unit_id != pi.unit_id
+ or Decimal(str(keep.quantity)) != total_qty
+ or Decimal(str(keep.unit_price)) != new_unit_price
+ or Decimal(str(keep.amount)) != new_amount
+ or (product and (keep.product_name != product.name or keep.image_url != product.image_url))
+ or keep.unit_name != (unit.name if unit else keep.unit_name)
+ or keep.category_name != (cat_name or keep.category_name)
+ )
+ if need_update:
+ self._copy_to_his(db, keep)
+ keep.price_list_id = pl.id
+ keep.unit_id = pi.unit_id
+ keep.quantity = total_qty
+ keep.unit_price = new_unit_price
+ keep.amount = new_amount
+ keep.product_name = product.name if product else keep.product_name
+ keep.unit_name = unit.name if unit else keep.unit_name
+ keep.category_name = cat_name or ""
+ keep.image_url = product.image_url if product else keep.image_url
+ keep.update_by = op_uid
+ keep.update_at = now
+ changed = True
+
+ if changed:
+ db.commit()
+ return True
+
+ def get(self, db: Session, cart_id: str) -> Optional[BizClientCart]:
+ return (
+ db.query(BizClientCart)
+ .filter(and_(BizClientCart.id == cart_id, BizClientCart.is_del == 0))
+ .first()
+ )
+
+ def _copy_to_his(self, db: Session, row: BizClientCart) -> None:
+ his = BizClientCartHis(
+ id=row.id,
+ client_id=row.client_id,
+ price_list_id=row.price_list_id,
+ product_id=row.product_id,
+ unit_id=row.unit_id,
+ quantity=row.quantity,
+ unit_price=row.unit_price,
+ amount=row.amount,
+ product_name=row.product_name,
+ unit_name=row.unit_name,
+ category_name=row.category_name,
+ image_url=row.image_url,
+ create_at=row.create_at,
+ create_by=row.create_by,
+ create_name=row.create_name,
+ update_at=row.update_at,
+ update_by=row.update_by,
+ his_create_at=datetime.now(),
+ )
+ db.add(his)
+
+ def add_or_update(
+ self,
+ db: Session,
+ client_id: str,
+ price_list_id: str,
+ product_id: str,
+ unit_id: str,
+ quantity: Decimal,
+ unit_price: Decimal,
+ product_name: str,
+ unit_name: str,
+ category_name: str,
+ image_url: str,
+ operator: dict,
+ ) -> BizClientCart:
+ matched = (
+ db.query(BizClientCart)
+ .filter(
+ and_(
+ BizClientCart.client_id == client_id,
+ BizClientCart.product_id == product_id,
+ BizClientCart.price_list_id == price_list_id,
+ BizClientCart.is_del == 0,
+ )
+ )
+ .all()
+ )
+ op_uid = operator.get("user_id")
+ now = datetime.now()
+ if matched:
+ # 兼容历史脏数据:同客户+同价目表+同商品可能存在多行,先合并再做增量更新。
+ keep = matched[0]
+ merged_qty = sum(Decimal(str(r.quantity or 0)) for r in matched)
+ if merged_qty < 0:
+ merged_qty = Decimal("0")
+
+ for row in matched[1:]:
+ self._copy_to_his(db, row)
+ row.is_del = 1
+ row.update_by = op_uid
+ row.update_at = now
+
+ self._copy_to_his(db, keep)
+ next_qty = merged_qty + Decimal(str(quantity))
+ amount = (next_qty * unit_price).quantize(Decimal("0.01"))
+ keep.quantity = next_qty
+ keep.unit_price = unit_price
+ keep.amount = amount
+ keep.product_name = product_name
+ keep.unit_name = unit_name
+ keep.category_name = category_name
+ keep.image_url = image_url
+ keep.update_by = op_uid
+ keep.update_at = now
+ db.commit()
+ return keep
+ amount = (quantity * unit_price).quantize(Decimal("0.01"))
+ row = BizClientCart(
+ id=generate_id(),
+ client_id=client_id,
+ price_list_id=price_list_id,
+ product_id=product_id,
+ unit_id=unit_id,
+ quantity=quantity,
+ unit_price=unit_price,
+ amount=amount,
+ product_name=product_name,
+ unit_name=unit_name,
+ category_name=category_name or "",
+ image_url=image_url,
+ is_del=0,
+ create_by=op_uid,
+ create_name=operator.get("real_name"),
+ update_by=op_uid,
+ )
+ db.add(row)
+ db.commit()
+ return row
+
+ def update_quantity(
+ self,
+ db: Session,
+ cart_id: str,
+ client_id: str,
+ quantity: Decimal,
+ operator: dict,
+ current_row: Optional[BizClientCart] = None,
+ ) -> Optional[BizClientCart]:
+ row = current_row or self.get(db, cart_id)
+ if not row or row.client_id != client_id:
+ return None
+ self._copy_to_his(db, row)
+ row.quantity = quantity
+ row.amount = (quantity * row.unit_price).quantize(Decimal("0.01"))
+ row.update_by = operator.get("user_id")
+ row.update_at = datetime.now()
+ db.commit()
+ return row
+
+ def soft_delete(self, db: Session, cart_id: str, client_id: str, operator: dict) -> bool:
+ row = self.get(db, cart_id)
+ if not row or row.client_id != client_id:
+ return False
+ self._copy_to_his(db, row)
+ row.is_del = 1
+ row.update_by = operator.get("user_id")
+ row.update_at = datetime.now()
+ db.commit()
+ return True
+
+ def clear(self, db: Session, client_id: str, operator: dict) -> int:
+ rows = self.list_by_client(db, client_id)
+ n = 0
+ for row in rows:
+ self._copy_to_his(db, row)
+ row.is_del = 1
+ row.update_by = operator.get("user_id")
+ row.update_at = datetime.now()
+ n += 1
+ if n:
+ db.commit()
+ return n
+
+ def batch_delete(self, db: Session, client_id: str, cart_ids: List[str], operator: dict) -> int:
+ n = 0
+ for cid in cart_ids:
+ if self.soft_delete(db, cid, client_id, operator):
+ n += 1
+ return n
+
+
+client_cart_crud = ClientCartCRUD()
diff --git a/backend/app/crud/deliverer_review.py b/backend/app/crud/deliverer_review.py
new file mode 100644
index 00000000..4db8fee7
--- /dev/null
+++ b/backend/app/crud/deliverer_review.py
@@ -0,0 +1,66 @@
+"""配送员评价 CRUD"""
+from typing import Optional, List
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, func
+
+from app.crud.base import CRUDBase
+from app.core.utils import generate_id
+from app.models.models import BizDelivererReview
+
+
+class DelivererReviewCRUD(CRUDBase[BizDelivererReview]):
+ """配送员评价 CRUD"""
+
+ def __init__(self):
+ super().__init__(BizDelivererReview)
+
+ def get_by_order(self, db: Session, order_id: str) -> Optional[BizDelivererReview]:
+ return db.query(self.model).filter(
+ and_(
+ self.model.order_id == order_id,
+ self.model.is_del == 0
+ )
+ ).first()
+
+ def create_review(
+ self,
+ db: Session,
+ order_id: str,
+ task_id: Optional[str],
+ client_id: str,
+ deliverer_id: str,
+ score: int,
+ tags: Optional[List[str]] = None,
+ content: Optional[str] = None,
+ is_anonymous: bool = False,
+ creator: Optional[dict] = None
+ ) -> BizDelivererReview:
+ data = {
+ "id": generate_id(),
+ "order_id": order_id,
+ "task_id": task_id,
+ "client_id": client_id,
+ "deliverer_id": deliverer_id,
+ "score": score,
+ "tags": ",".join(tags) if tags else None,
+ "content": content,
+ "is_anonymous": is_anonymous,
+ "create_by": creator.get("user_id") if creator else None,
+ "create_name": creator.get("real_name") if creator else None
+ }
+ return self.create(db, data)
+
+ def get_deliverer_rating_stats(self, db: Session, deliverer_id: str) -> dict:
+ total = db.query(func.count(self.model.id)).filter(
+ and_(self.model.deliverer_id == deliverer_id, self.model.is_del == 0)
+ ).scalar() or 0
+ avg_score = db.query(func.avg(self.model.score)).filter(
+ and_(self.model.deliverer_id == deliverer_id, self.model.is_del == 0)
+ ).scalar()
+ return {
+ "total": int(total),
+ "avgScore": round(float(avg_score), 2) if avg_score is not None else 0.0
+ }
+
+
+deliverer_review_crud = DelivererReviewCRUD()
diff --git a/backend/app/crud/delivery.py b/backend/app/crud/delivery.py
new file mode 100644
index 00000000..99a1dfb4
--- /dev/null
+++ b/backend/app/crud/delivery.py
@@ -0,0 +1,872 @@
+"""配送任务CRUD"""
+import json
+from typing import Optional, List
+from datetime import datetime, timedelta
+from decimal import Decimal
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, func, or_, select, exists
+
+from app.models.models import BizDeliveryTask, BizDeliveryTaskItem, BizDeliveryTaskHis, BizOrder
+from app.crud.base import CRUDBase
+from app.core.utils import generate_id
+
+
+class DeliveryTaskCRUD(CRUDBase[BizDeliveryTask]):
+ """配送任务CRUD操作"""
+
+ @staticmethod
+ def _sync_legacy_status_from_dual(task: BizDeliveryTask) -> None:
+ """根据双状态回写兼容字段 status。"""
+ ds = (getattr(task, "deliverer_status", None) or "").strip()
+ ss = (getattr(task, "supplier_prepare_status", None) or "").strip()
+ if ds == "delivered":
+ task.status = "completed"
+ elif ds == "delivering":
+ task.status = "delivering"
+ elif ss == "stocking":
+ task.status = "supplier_stocking"
+ elif ss == "prepared":
+ task.status = "supplier_ready"
+ else:
+ task.status = "assigned" if task.deliver_id else "pending"
+
+ def get_by_order(self, db: Session, order_id: str) -> Optional[BizDeliveryTask]:
+ """根据订单ID获取配送任务"""
+ return db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.order_id == order_id,
+ BizDeliveryTask.is_del == 0
+ )
+ ).first()
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ region_id: str = None,
+ supplier_id: str = None,
+ status: str = None,
+ supplier_prepare_status: str = None,
+ order_no: str = None,
+ deliver_id: str = None,
+ keyword: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizDeliveryTask], int]:
+ """多条件查询配送任务"""
+ query = db.query(BizDeliveryTask).filter(BizDeliveryTask.is_del == 0)
+
+ if region_id:
+ query = query.filter(BizDeliveryTask.region_id == region_id)
+ if supplier_id:
+ query = query.filter(BizDeliveryTask.supplier_id == supplier_id)
+ if supplier_prepare_status:
+ parts = [s.strip() for s in supplier_prepare_status.split(",") if s.strip()]
+ if parts:
+ if len(parts) > 1:
+ query = query.filter(BizDeliveryTask.supplier_prepare_status.in_(parts))
+ else:
+ p0 = parts[0]
+ if p0 == "pending_prepare":
+ query = query.filter(
+ or_(
+ BizDeliveryTask.supplier_prepare_status == "pending_prepare",
+ BizDeliveryTask.supplier_prepare_status.is_(None),
+ )
+ )
+ else:
+ query = query.filter(BizDeliveryTask.supplier_prepare_status == p0)
+ elif status:
+ parts = [s.strip() for s in status.split(",") if s.strip()]
+ if parts:
+ if len(parts) > 1:
+ query = query.filter(BizDeliveryTask.status.in_(parts))
+ else:
+ query = query.filter(BizDeliveryTask.status == parts[0])
+ if order_no:
+ query = query.filter(BizDeliveryTask.task_no.like(f"%{order_no}%"))
+ if deliver_id:
+ query = query.filter(BizDeliveryTask.deliver_id == deliver_id)
+ if keyword:
+ kw = (keyword or "").strip()
+ if kw:
+ pattern = f"%{kw}%"
+ order_match = exists(
+ select(1).select_from(BizOrder).where(
+ and_(
+ BizOrder.id == BizDeliveryTask.order_id,
+ BizOrder.is_del == 0,
+ or_(
+ BizOrder.order_no.like(pattern),
+ BizOrder.contact_person.like(pattern),
+ BizOrder.contact_phone.like(pattern),
+ ),
+ )
+ )
+ )
+ query = query.filter(
+ or_(
+ BizDeliveryTask.task_no.like(pattern),
+ BizDeliveryTask.order_id.like(pattern),
+ order_match,
+ )
+ )
+
+ total = query.count()
+ tasks = query.order_by(BizDeliveryTask.create_at.desc()).offset(skip).limit(limit).all()
+ return tasks, total
+
+ def get_by_deliver(
+ self,
+ db: Session,
+ deliver_id: str,
+ status: str = None,
+ deliverer_status: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizDeliveryTask], int]:
+ """根据配送员获取配送任务。优先 deliverer_status(小程序);否则按兼容字段 status。"""
+ query = db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.deliver_id == deliver_id,
+ BizDeliveryTask.is_del == 0,
+ BizDeliveryTask.status != "cancelled",
+ )
+ )
+
+ if deliverer_status:
+ parts = [s.strip() for s in deliverer_status.split(",") if s.strip()]
+ if parts:
+ if len(parts) > 1:
+ query = query.filter(BizDeliveryTask.deliverer_status.in_(parts))
+ elif parts[0] == "pending_pickup":
+ query = query.filter(
+ or_(
+ BizDeliveryTask.deliverer_status == "pending_pickup",
+ BizDeliveryTask.deliverer_status.is_(None),
+ )
+ )
+ else:
+ query = query.filter(BizDeliveryTask.deliverer_status == parts[0])
+ elif status:
+ parts = [s.strip() for s in status.split(",") if s.strip()]
+ if parts:
+ if len(parts) > 1:
+ query = query.filter(BizDeliveryTask.status.in_(parts))
+ else:
+ query = query.filter(BizDeliveryTask.status == parts[0])
+
+ total = query.count()
+ tasks = query.order_by(BizDeliveryTask.create_at.desc()).offset(skip).limit(limit).all()
+ return tasks, total
+
+ def get_by_supplier(
+ self,
+ db: Session,
+ supplier_id: str,
+ status: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizDeliveryTask], int]:
+ """根据供应商获取配送任务"""
+ query = db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.supplier_id == supplier_id,
+ BizDeliveryTask.is_del == 0
+ )
+ )
+
+ if status:
+ query = query.filter(BizDeliveryTask.status == status)
+
+ total = query.count()
+ tasks = query.order_by(BizDeliveryTask.create_at.desc()).offset(skip).limit(limit).all()
+ return tasks, total
+
+ def get_pending_list(
+ self,
+ db: Session,
+ region_id: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizDeliveryTask], int]:
+ """获取待分配配送的任务"""
+ query = db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.status == "pending",
+ BizDeliveryTask.is_del == 0
+ )
+ )
+
+ if region_id:
+ query = query.filter(BizDeliveryTask.region_id == region_id)
+
+ total = query.count()
+ tasks = query.order_by(BizDeliveryTask.create_at.asc()).offset(skip).limit(limit).all()
+ return tasks, total
+
+ def assign_deliver(
+ self,
+ db: Session,
+ order_id: str,
+ deliver_id: str,
+ operator_id: str = None
+ ) -> Optional[BizDeliveryTask]:
+ """分配配送员"""
+ task = self.get_by_order(db, order_id)
+ if not task:
+ return None
+
+ if task.status != "pending":
+ return None
+
+ task.deliver_id = deliver_id
+ task.deliverer_status = "pending_pickup"
+ task.supplier_prepare_status = "pending_prepare"
+ self._sync_legacy_status_from_dual(task)
+ task.update_at = datetime.now()
+
+ db.commit()
+ db.refresh(task)
+ return task
+
+ def assign_deliver_batch(
+ self,
+ db: Session,
+ assignments: List[dict]
+ ) -> List[BizDeliveryTask]:
+ """批量分配配送员"""
+ results = []
+ for assignment in assignments:
+ order_id = assignment.get("orderId")
+ deliver_id = assignment.get("deliverId")
+ task = self.assign_deliver(db, order_id, deliver_id)
+ if task:
+ results.append(task)
+ return results
+
+ def update_status(
+ self,
+ db: Session,
+ task_id: str,
+ new_status: str,
+ operator_id: str = None,
+ remark: str = None,
+ image_urls: List[str] = None,
+ operator_real_name: str = None,
+ operator_role: str = "deliver",
+ ) -> Optional[BizDeliveryTask]:
+ """更新配送任务状态"""
+ task = self.get(db, task_id)
+ if not task:
+ return None
+
+ old_status = task.status
+ task.status = new_status
+ task.update_at = datetime.now()
+
+ if new_status == "picking":
+ # 配送员已取货:进入配送中(勿再保留 pending_pickup,否则列表/标题仍显示「待取货」)
+ task.deliverer_status = "delivering"
+ task.supplier_prepare_status = "picked_up"
+ task.pickup_time = datetime.now()
+ if remark:
+ task.pickup_remark = remark
+ if image_urls:
+ task.pickup_image_url = ",".join(image_urls) if len(image_urls) > 0 else None
+ elif new_status == "supplier_stocking":
+ task.supplier_prepare_status = "stocking"
+ elif new_status == "supplier_ready":
+ task.supplier_prepare_status = "prepared"
+ # 备货完成凭证(与配送员取货凭证共用字段,时间线区分为供应商操作)
+ if remark:
+ task.pickup_remark = remark
+ if image_urls:
+ task.pickup_image_url = ",".join(image_urls) if len(image_urls) > 0 else None
+ elif new_status == "delivering":
+ task.deliverer_status = "delivering"
+ if remark:
+ task.delivery_remark = remark
+ if image_urls:
+ task.delivery_image_url = ",".join(image_urls) if len(image_urls) > 0 else None
+
+ elif new_status == "completed":
+ task.deliverer_status = "delivered"
+ # 送达确认时间:与模型注释一致;小程序取货后直达 completed,必须在此写入
+ task.delivery_time = datetime.now()
+ if remark:
+ task.delivery_remark = remark
+ if image_urls:
+ task.delivery_image_url = ",".join(image_urls) if len(image_urls) > 0 else None
+
+ self._sync_legacy_status_from_dual(task)
+
+ # 写入订单「操作记录」:legacy task.status 若已为 picking,会与取货入参 new_status 相同而漏记;
+ # 配送员确认取货时必须仍写入一条。
+ if task.order_id:
+ from app.crud.order import order_crud
+
+ should_log_order = (old_status != new_status) or new_status == "picking"
+ if should_log_order:
+ order_crud._create_status_log(
+ db,
+ task.order_id,
+ f"dt_{old_status}" if old_status is not None else "",
+ f"dt_{new_status}",
+ operator_id or "system",
+ operator_role or "deliver",
+ reason=remark,
+ create_name=(operator_real_name or "").strip() or None,
+ )
+
+ db.commit()
+ db.refresh(task)
+
+ if new_status == "delivering":
+ from app.crud.delivery_warning_resolve import auto_resolve_open_warnings_for_task
+
+ auto_resolve_open_warnings_for_task(db, task_id)
+
+ return task
+
+ def cancel_task(
+ self,
+ db: Session,
+ task_id: str,
+ reason: str = None,
+ operator_id: str = None,
+ operator_real_name: str = None,
+ ) -> Optional[BizDeliveryTask]:
+ """取消配送任务"""
+ task = self.get(db, task_id)
+ if not task:
+ return None
+
+ if task.status in ["completed", "cancelled"]:
+ return None
+
+ old_status = task.status
+ task.status = "cancelled"
+ task.update_at = datetime.now()
+
+ if task.order_id:
+ from app.crud.order import order_crud
+
+ order_crud._create_status_log(
+ db,
+ task.order_id,
+ f"dt_{old_status}",
+ "dt_cancelled",
+ operator_id or "system",
+ "admin",
+ reason=reason,
+ create_name=(operator_real_name or "").strip() or None,
+ )
+
+ db.commit()
+ db.refresh(task)
+
+ from app.crud.delivery_warning_resolve import auto_resolve_open_warnings_for_task
+
+ auto_resolve_open_warnings_for_task(db, task_id)
+
+ return task
+
+ def reassign(
+ self,
+ db: Session,
+ task_id: str,
+ new_deliver_id: str,
+ operator_id: str = None,
+ operator_real_name: str = None,
+ ) -> Optional[BizDeliveryTask]:
+ """重新分配配送员"""
+ from app.crud.order import order_crud
+ from app.crud.user import user_crud
+
+ task = self.get(db, task_id)
+ if not task:
+ return None
+
+ if task.status in ["completed", "cancelled"]:
+ return None
+
+ old_deliver_id = task.deliver_id
+ if old_deliver_id == new_deliver_id:
+ return task
+
+ updater = None
+ if operator_id:
+ updater = {"user_id": operator_id, "real_name": operator_real_name}
+
+ self.update_with_history(
+ db,
+ task_id,
+ {"deliver_id": new_deliver_id},
+ updater=updater,
+ )
+ task = self.get(db, task_id)
+ if not task:
+ return None
+
+ def _dn(uid: Optional[str]) -> Optional[str]:
+ if not uid:
+ return None
+ u = user_crud.get(db, uid)
+ return (u.real_name if u else None) or uid
+
+ nu = user_crud.get(db, new_deliver_id)
+ order_crud._create_status_log(
+ db,
+ task.order_id,
+ "",
+ "delivery.reassign",
+ operator_id or "system",
+ "admin",
+ reason=json.dumps(
+ {
+ "fromDeliverId": old_deliver_id,
+ "toDeliverId": new_deliver_id,
+ "fromDeliverName": _dn(old_deliver_id),
+ "toDeliverName": nu.real_name if nu else new_deliver_id,
+ },
+ ensure_ascii=False,
+ ),
+ create_name=(operator_real_name or "").strip() or None,
+ )
+ db.commit()
+ db.refresh(task)
+ return task
+
+ def get_warning_tasks(
+ self,
+ db: Session,
+ region_id: str = None,
+ within_minutes: int = 60,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizDeliveryTask], int, int]:
+ """获取时效预警任务"""
+ now = datetime.now()
+ expire_time_threshold = now + timedelta(minutes=within_minutes)
+
+ query = db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.status.in_(["pending", "assigned", "picking"]),
+ BizDeliveryTask.is_del == 0,
+ BizDeliveryTask.effective_time <= expire_time_threshold
+ )
+ )
+
+ if region_id:
+ query = query.filter(BizDeliveryTask.region_id == region_id)
+
+ total = query.count()
+ warning_count = query.filter(
+ BizDeliveryTask.effective_time <= now + timedelta(minutes=30)
+ ).count()
+
+ tasks = query.order_by(BizDeliveryTask.effective_time.asc()).offset(skip).limit(limit).all()
+ return tasks, total, warning_count
+
+ def _generate_task_no(self, db: Session) -> str:
+ """生成配送单号"""
+ today = datetime.now().strftime("%Y%m%d")
+ prefix = f"DLV{today}"
+
+ last_task = db.query(BizDeliveryTask).filter(
+ BizDeliveryTask.task_no.like(f"{prefix}%")
+ ).order_by(BizDeliveryTask.task_no.desc()).first()
+
+ if last_task:
+ last_no = int(last_task.task_no[-4:])
+ new_no = last_no + 1
+ else:
+ new_no = 1
+
+ return f"{prefix}{str(new_no).zfill(4)}"
+
+ def create_from_order(
+ self,
+ db: Session,
+ order_id: str,
+ supplier_id: str,
+ region_id: str,
+ creator: dict = None
+ ) -> BizDeliveryTask:
+ """从订单创建配送任务"""
+ task_no = self._generate_task_no(db)
+
+ # 设置配送截止时间(默认4小时)
+ effective_time = datetime.now() + timedelta(hours=4)
+
+ task = BizDeliveryTask(
+ id=generate_id(),
+ task_no=task_no,
+ order_id=order_id,
+ region_id=region_id,
+ supplier_id=supplier_id,
+ status="pending",
+ deliverer_status="pending_pickup",
+ supplier_prepare_status="pending_prepare",
+ effective_time=effective_time,
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(task)
+ db.flush()
+
+ return task
+
+ def create_delivery_task_from_order(
+ self,
+ db: Session,
+ order_id: str,
+ order_items: List[dict],
+ supplier_id: str,
+ region_id: str,
+ creator: dict = None
+ ) -> tuple:
+ """从订单创建完整的配送任务(包含明细)"""
+ from app.crud.order import order_item_crud
+
+ # 创建配送任务
+ task = self.create_from_order(
+ db, order_id, supplier_id, region_id, creator
+ )
+
+ # 创建配送任务明细
+ task_items = []
+ for item_data in order_items:
+ # 获取订单项信息
+ order_item = order_item_crud.get(db, item_data.get("order_item_id"))
+ if not order_item:
+ continue
+
+ task_item = BizDeliveryTaskItem(
+ id=generate_id(),
+ task_id=task.id,
+ order_id=order_id,
+ order_item_id=item_data.get("order_item_id"),
+ product_id=item_data.get("product_id"),
+ client_id=item_data.get("client_id"),
+ delivery_address=item_data.get("delivery_address"),
+ contact_person=item_data.get("contact_person"),
+ contact_phone=item_data.get("contact_phone"),
+ quantity=Decimal(str(item_data.get("quantity", 0))),
+ status="pending",
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(task_item)
+ task_items.append(task_item)
+
+ db.commit()
+ db.refresh(task)
+ for item in task_items:
+ db.refresh(item)
+
+ return task, task_items
+
+ def get_expiring_tasks(
+ self,
+ db: Session,
+ within_minutes: int = 60,
+ region_id: str = None
+ ) -> List[BizDeliveryTask]:
+ """获取即将过期的配送任务(用于定时任务)"""
+ now = datetime.now()
+ expire_threshold = now + timedelta(minutes=within_minutes)
+
+ query = db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.status.in_(["pending", "assigned", "picking"]),
+ BizDeliveryTask.is_del == 0,
+ BizDeliveryTask.effective_time <= expire_threshold,
+ BizDeliveryTask.effective_time >= now
+ )
+ )
+
+ if region_id:
+ query = query.filter(BizDeliveryTask.region_id == region_id)
+
+ return query.order_by(BizDeliveryTask.effective_time.asc()).all()
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizDeliveryTask]:
+ """更新配送任务并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": generate_id(),
+ "task_no": original.task_no,
+ "order_id": original.order_id,
+ "region_id": original.region_id,
+ "supplier_id": original.supplier_id,
+ "deliver_id": original.deliver_id,
+ "status": original.status,
+ "deliverer_status": getattr(original, "deliverer_status", None),
+ "supplier_prepare_status": getattr(original, "supplier_prepare_status", None),
+ "pickup_time": original.pickup_time,
+ "pickup_remark": original.pickup_remark,
+ "pickup_image_url": original.pickup_image_url,
+ "delivery_time": original.delivery_time,
+ "delivery_remark": original.delivery_remark,
+ "delivery_image_url": original.delivery_image_url,
+ "expire_warning_sent": getattr(original, "expire_warning_sent", False),
+ "effective_time": original.effective_time,
+ "dispatch_remark": getattr(original, "dispatch_remark", None),
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizDeliveryTaskHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ # =========================================
+ # 抢单相关方法
+ # =========================================
+
+ def get_pending_snatch_tasks(
+ self,
+ db: Session,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizDeliveryTask], int]:
+ """获取待抢单的任务(状态为pending且未分配配送员)"""
+ query = db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.status == "pending",
+ BizDeliveryTask.is_del == 0
+ )
+ )
+
+ total = query.count()
+ tasks = query.order_by(BizDeliveryTask.effective_time.asc()).offset(skip).limit(limit).all()
+ return tasks, total
+
+ def count_pending_snatch_tasks(self, db: Session) -> int:
+ """统计待抢单的任务数量"""
+ return db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.status == "pending",
+ BizDeliveryTask.is_del == 0
+ )
+ ).count()
+
+ def assign_to_deliver(
+ self,
+ db: Session,
+ task_id: str,
+ deliver_id: str,
+ deliver_name: str = None
+ ) -> bool:
+ """分配任务给配送员(用于抢单)"""
+ task = self.get(db, task_id)
+ if not task:
+ return False
+
+ if task.status != "pending":
+ return False
+
+ if task.deliver_id is not None:
+ return False
+
+ task.deliver_id = deliver_id
+ task.deliver_name = deliver_name
+ task.deliverer_status = "pending_pickup"
+ task.supplier_prepare_status = "pending_prepare"
+ self._sync_legacy_status_from_dual(task)
+ task.update_at = datetime.now()
+
+ db.commit()
+ db.refresh(task)
+ return True
+
+ def get_snatched_by_me(
+ self,
+ db: Session,
+ deliver_id: str,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizDeliveryTask], int]:
+ """获取我抢到的任务"""
+ query = db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.deliver_id == deliver_id,
+ BizDeliveryTask.status.in_(["pending", "assigned"]),
+ BizDeliveryTask.is_del == 0
+ )
+ )
+
+ total = query.count()
+ tasks = query.order_by(BizDeliveryTask.effective_time.asc()).offset(skip).limit(limit).all()
+ return tasks, total
+
+ def count_snatched_by_me(self, db: Session, deliver_id: str) -> int:
+ """统计我抢到的任务数量"""
+ return db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.deliver_id == deliver_id,
+ BizDeliveryTask.status.in_(["pending", "assigned"]),
+ BizDeliveryTask.is_del == 0
+ )
+ ).count()
+
+ def count_today_snatched(self, db: Session, deliver_id: str, start_time: datetime) -> int:
+ """统计今日抢单成功数量"""
+ return db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.deliver_id == deliver_id,
+ BizDeliveryTask.status.in_(["pending", "assigned", "picking", "delivering", "completed"]),
+ BizDeliveryTask.is_del == 0,
+ BizDeliveryTask.update_at >= start_time
+ )
+ ).count()
+
+ def cancel_snatch(self, db: Session, task_id: str) -> bool:
+ """取消抢单"""
+ task = self.get(db, task_id)
+ if not task:
+ return False
+
+ if task.status not in ["pending", "assigned"]:
+ return False
+
+ task.deliver_id = None
+ task.deliver_name = None
+ task.status = "pending"
+ task.update_at = datetime.now()
+
+ db.commit()
+ db.refresh(task)
+ return True
+
+ def get_history_by_deliver(
+ self,
+ db: Session,
+ deliver_id: str,
+ start_date: str = None,
+ end_date: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizDeliveryTask], int]:
+ """获取配送员历史任务(已完成和已取消的)"""
+ query = db.query(BizDeliveryTask).filter(
+ and_(
+ BizDeliveryTask.deliver_id == deliver_id,
+ BizDeliveryTask.status.in_(["completed", "cancelled"]),
+ BizDeliveryTask.is_del == 0
+ )
+ )
+
+ if start_date:
+ query = query.filter(BizDeliveryTask.create_at >= start_date)
+ if end_date:
+ query = query.filter(BizDeliveryTask.create_at <= end_date + " 23:59:59")
+
+ total = query.count()
+ tasks = query.order_by(BizDeliveryTask.delivery_time.desc()).offset(skip).limit(limit).all()
+ return tasks, total
+
+
+class DeliveryTaskItemCRUD(CRUDBase[BizDeliveryTaskItem]):
+ """配送任务明细CRUD操作"""
+
+ def get_by_task(self, db: Session, task_id: str) -> List[BizDeliveryTaskItem]:
+ """获取配送任务的所有明细"""
+ return db.query(BizDeliveryTaskItem).filter(
+ and_(
+ BizDeliveryTaskItem.task_id == task_id,
+ BizDeliveryTaskItem.is_del == 0
+ )
+ ).all()
+
+ def get_delivered_quantity_by_order_item(
+ self, db: Session, order_id: str
+ ) -> dict[str, float]:
+ """按订单行汇总当前配送任务明细上的已送达数量(与 biz_order_item.delivered_quantity 对齐展示用)"""
+ task = db.query(BizDeliveryTask).filter(
+ and_(BizDeliveryTask.order_id == order_id, BizDeliveryTask.is_del == 0)
+ ).first()
+ if not task:
+ return {}
+ out: dict[str, float] = {}
+ for ti in self.get_by_task(db, task.id):
+ oid = ti.order_item_id
+ if not oid:
+ continue
+ q = float(ti.delivered_quantity or 0)
+ prev = out.get(oid, 0.0)
+ if q > prev:
+ out[oid] = q
+ return out
+
+ def create_items(
+ self,
+ db: Session,
+ task_id: str,
+ items: List[dict],
+ creator: dict = None
+ ) -> List[BizDeliveryTaskItem]:
+ """批量创建配送任务明细"""
+ result = []
+ for item in items:
+ task_item = BizDeliveryTaskItem(
+ id=generate_id(),
+ task_id=task_id,
+ order_id=item.get("order_id"),
+ order_item_id=item.get("order_item_id"),
+ product_id=item.get("product_id"),
+ client_id=item.get("client_id"),
+ delivery_address=item.get("delivery_address"),
+ contact_person=item.get("contact_person"),
+ contact_phone=item.get("contact_phone"),
+ quantity=Decimal(str(item.get("quantity", 0))),
+ status="pending",
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(task_item)
+ result.append(task_item)
+ db.commit()
+ for item in result:
+ db.refresh(item)
+ return result
+
+ def update_item_status(
+ self,
+ db: Session,
+ item_id: str,
+ delivered_quantity: Decimal,
+ status: str = "delivered"
+ ) -> Optional[BizDeliveryTaskItem]:
+ """更新配送明细状态"""
+ item = self.get(db, item_id)
+ if not item:
+ return None
+
+ item.delivered_quantity = delivered_quantity
+ item.status = status
+ if status == "delivered":
+ item.delivery_time = datetime.now()
+
+ if item.order_item_id:
+ from app.crud.order import order_item_crud
+
+ oi = order_item_crud.get(db, item.order_item_id)
+ if oi is not None:
+ oi.delivered_quantity = delivered_quantity
+ oi.update_at = datetime.now()
+
+ db.commit()
+ db.refresh(item)
+ return item
+
+
+delivery_task_crud = DeliveryTaskCRUD(BizDeliveryTask)
+delivery_task_item_crud = DeliveryTaskItemCRUD(BizDeliveryTaskItem)
diff --git a/backend/app/crud/delivery_warning_resolve.py b/backend/app/crud/delivery_warning_resolve.py
new file mode 100644
index 00000000..dc2e5fd9
--- /dev/null
+++ b/backend/app/crud/delivery_warning_resolve.py
@@ -0,0 +1,55 @@
+"""配送预警:任务完成/取消时自动结案(写历史后更新主表)"""
+from datetime import datetime
+
+from sqlalchemy.orm import Session
+
+from app.models.models import BizDeliveryWarning, BizDeliveryWarningHis
+
+
+def auto_resolve_open_warnings_for_task(db: Session, task_id: str) -> int:
+ """将某配送任务下未结案预警全部自动处理(配送完成等场景)。"""
+ warnings = (
+ db.query(BizDeliveryWarning)
+ .filter(
+ BizDeliveryWarning.task_id == task_id,
+ BizDeliveryWarning.is_del == 0,
+ BizDeliveryWarning.is_resolved == 0,
+ )
+ .all()
+ )
+ if not warnings:
+ return 0
+
+ now = datetime.now()
+ for warning in warnings:
+ his = BizDeliveryWarningHis(
+ id=warning.id,
+ task_id=warning.task_id,
+ order_id=warning.order_id,
+ warning_type=warning.warning_type,
+ urgency_level=warning.urgency_level,
+ remaining_minutes=warning.remaining_minutes,
+ message=warning.message,
+ is_sent=warning.is_sent,
+ sent_time=warning.sent_time,
+ is_resolved=warning.is_resolved,
+ resolved_time=warning.resolved_time,
+ resolved_by=warning.resolved_by,
+ resolved_note=warning.resolved_note,
+ create_at=warning.create_at,
+ create_by=warning.create_by,
+ create_name=warning.create_name,
+ update_at=warning.update_at,
+ update_by=warning.update_by,
+ his_create_at=now,
+ )
+ db.add(his)
+ warning.is_resolved = 1
+ warning.resolved_time = now
+ warning.resolved_by = "system"
+ warning.resolved_note = "auto:配送已完成"
+ warning.update_at = now
+ warning.update_by = "system"
+
+ db.commit()
+ return len(warnings)
diff --git a/backend/app/crud/inquiry.py b/backend/app/crud/inquiry.py
new file mode 100644
index 00000000..bae71e37
--- /dev/null
+++ b/backend/app/crud/inquiry.py
@@ -0,0 +1,231 @@
+"""询价单CRUD"""
+from typing import Optional, List
+from datetime import datetime
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, func
+
+from app.models.models import BizInquiry, BizInquiryItem, BizInquiryItemHis, BizInquiryHis
+from app.crud.base import CRUDBase
+from app.core.utils import generate_id
+
+
+class InquiryCRUD(CRUDBase[BizInquiry]):
+ """询价单CRUD操作"""
+
+ def get_by_region(
+ self,
+ db: Session,
+ region_id: str,
+ status: str = None,
+ skip: int = 0,
+ limit: int = 20
+ ) -> List[BizInquiry]:
+ """根据区域获取询价单列表"""
+ query = db.query(BizInquiry).filter(
+ and_(
+ BizInquiry.region_id == region_id,
+ BizInquiry.is_del == 0
+ )
+ )
+ if status:
+ query = query.filter(BizInquiry.status == status)
+ return query.order_by(BizInquiry.create_at.desc()).offset(skip).limit(limit).all()
+
+ def get_published_by_region(
+ self,
+ db: Session,
+ region_id: str,
+ skip: int = 0,
+ limit: int = 20
+ ) -> List[BizInquiry]:
+ """获取区域已发布的询价单列表"""
+ return self.get_by_region(db, region_id, status="published", skip=skip, limit=limit)
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ region_id: str = None,
+ status: str = None,
+ keyword: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizInquiry], int]:
+ """多条件查询询价单"""
+ query = db.query(BizInquiry).filter(BizInquiry.is_del == 0)
+
+ if region_id:
+ query = query.filter(BizInquiry.region_id == region_id)
+ if status:
+ query = query.filter(BizInquiry.status == status)
+ if keyword:
+ query = query.filter(BizInquiry.title.like(f"%{keyword}%"))
+
+ total = query.count()
+ inquiries = query.order_by(BizInquiry.create_at.desc()).offset(skip).limit(limit).all()
+ return inquiries, total
+
+ def publish(self, db: Session, id: str) -> Optional[BizInquiry]:
+ """发布询价单"""
+ inquiry = self.get(db, id)
+ if inquiry and inquiry.status == "draft":
+ inquiry.status = "published"
+ inquiry.publish_time = datetime.now()
+ db.commit()
+ db.refresh(inquiry)
+ return inquiry
+
+ def unpublish(self, db: Session, id: str) -> Optional[BizInquiry]:
+ """取消发布询价单(published -> draft)"""
+ inquiry = self.get(db, id)
+ if inquiry and inquiry.status == "published":
+ inquiry.status = "draft"
+ # 保留 publish_time 作为历史痕迹;如需清空可在这里置 None
+ db.commit()
+ db.refresh(inquiry)
+ return inquiry
+
+ def get_current_published_by_region(self, db: Session, region_id: str) -> Optional[BizInquiry]:
+ """获取区域当前发布中的询价单(最多1条,业务上应保证互斥)"""
+ return db.query(BizInquiry).filter(
+ and_(
+ BizInquiry.region_id == region_id,
+ BizInquiry.status == "published",
+ BizInquiry.is_del == 0
+ )
+ ).order_by(BizInquiry.publish_time.desc(), BizInquiry.create_at.desc()).first()
+
+ def close(self, db: Session, id: str) -> Optional[BizInquiry]:
+ """关闭询价单"""
+ inquiry = self.get(db, id)
+ if inquiry and inquiry.status == "published":
+ inquiry.status = "closed"
+ inquiry.close_time = datetime.now()
+ db.commit()
+ db.refresh(inquiry)
+ return inquiry
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizInquiry]:
+ """更新询价单并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "region_id": original.region_id,
+ "title": original.title,
+ "status": original.status,
+ "publish_time": original.publish_time,
+ "close_time": original.close_time,
+ "remark": original.remark,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizInquiryHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, id: str) -> bool:
+ """删除询价单并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return False
+
+ his_data = {
+ "id": original.id,
+ "region_id": original.region_id,
+ "title": original.title,
+ "status": original.status,
+ "publish_time": original.publish_time,
+ "close_time": original.close_time,
+ "remark": original.remark,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizInquiryHis(**his_data)
+ db.add(his)
+
+ return self.delete(db, id)
+
+
+class InquiryItemCRUD(CRUDBase[BizInquiryItem]):
+ """询价单项CRUD操作"""
+
+ def get_by_inquiry(self, db: Session, inquiry_id: str) -> List[BizInquiryItem]:
+ """获取询价单的所有询价项"""
+ return db.query(BizInquiryItem).filter(
+ and_(
+ BizInquiryItem.inquiry_id == inquiry_id,
+ BizInquiryItem.is_del == 0
+ )
+ ).all()
+
+ def get_multi_with_inquiry_ids(
+ self,
+ db: Session,
+ inquiry_ids: List[str]
+ ) -> List[BizInquiryItem]:
+ """根据多个询价单ID获取询价项"""
+ return db.query(BizInquiryItem).filter(
+ and_(
+ BizInquiryItem.inquiry_id.in_(inquiry_ids),
+ BizInquiryItem.is_del == 0
+ )
+ ).all()
+
+ def create_items(
+ self,
+ db: Session,
+ inquiry_id: str,
+ product_ids: List[str],
+ creator: dict = None
+ ) -> List[BizInquiryItem]:
+ """批量创建询价项"""
+ items = []
+ for product_id in product_ids:
+ item = BizInquiryItem(
+ id=generate_id(),
+ inquiry_id=inquiry_id,
+ product_id=product_id,
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(item)
+ items.append(item)
+ db.commit()
+ for item in items:
+ db.refresh(item)
+ return items
+
+ def update_items(
+ self,
+ db: Session,
+ inquiry_id: str,
+ product_ids: List[str],
+ creator: dict = None
+ ) -> List[BizInquiryItem]:
+ """更新询价项(先删除旧的,再创建新的)"""
+ existing_items = self.get_by_inquiry(db, inquiry_id)
+ for item in existing_items:
+ item.is_del = 1
+
+ return self.create_items(db, inquiry_id, product_ids, creator)
+
+ def delete_by_inquiry(self, db: Session, inquiry_id: str) -> bool:
+ """删除询价单的所有询价项"""
+ items = self.get_by_inquiry(db, inquiry_id)
+ for item in items:
+ item.is_del = 1
+ db.commit()
+ return True
+
+
+inquiry_crud = InquiryCRUD(BizInquiry)
+inquiry_item_crud = InquiryItemCRUD(BizInquiryItem)
diff --git a/backend/app/crud/ord_settlement.py b/backend/app/crud/ord_settlement.py
new file mode 100644
index 00000000..dd918594
--- /dev/null
+++ b/backend/app/crud/ord_settlement.py
@@ -0,0 +1,314 @@
+"""客户/供应商结算单 CRUD(含 UPDATE 写历史表)"""
+from __future__ import annotations
+
+from datetime import datetime
+from typing import Any, Dict, List, Optional, Tuple
+
+from sqlalchemy.orm import Session
+
+from app.crud.base import CRUDBase
+from app.models.models import (
+ BizOrder,
+ OrdClientSettlement,
+ OrdClientSettlementHis,
+ OrdClientSettlementItem,
+ OrdClientSettlementItemHis,
+ OrdSupplierSettlement,
+ OrdSupplierSettlementHis,
+ OrdSupplierSettlementItem,
+ OrdSupplierSettlementItemHis,
+)
+
+
+class OrdClientSettlementCRUD(CRUDBase[OrdClientSettlement]):
+ def _snapshot_main(self, row: OrdClientSettlement) -> Dict[str, Any]:
+ return {
+ "id": row.id,
+ "settlement_no": row.settlement_no,
+ "client_id": row.client_id,
+ "client_name": row.client_name,
+ "order_count": row.order_count,
+ "total_amount": row.total_amount,
+ "voucher_urls": row.voucher_urls,
+ "remark": row.remark,
+ "settlement_at": row.settlement_at,
+ "status": row.status,
+ "create_at": row.create_at,
+ "create_by": row.create_by,
+ "create_name": row.create_name,
+ "update_at": row.update_at,
+ "update_by": row.update_by,
+ }
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ *,
+ client_id: Optional[str] = None,
+ settlement_no: Optional[str] = None,
+ start_date: Optional[datetime] = None,
+ end_date: Optional[datetime] = None,
+ region_id: Optional[str] = None,
+ skip: int = 0,
+ limit: int = 10,
+ ) -> Tuple[List[OrdClientSettlement], int]:
+ q = db.query(OrdClientSettlement).filter(OrdClientSettlement.is_del == 0)
+ if client_id:
+ q = q.filter(OrdClientSettlement.client_id == client_id)
+ if settlement_no:
+ q = q.filter(OrdClientSettlement.settlement_no.like(f"%{settlement_no}%"))
+ if start_date:
+ q = q.filter(OrdClientSettlement.settlement_at >= start_date)
+ if end_date:
+ q = q.filter(OrdClientSettlement.settlement_at <= end_date)
+ if region_id:
+ sub = (
+ db.query(OrdClientSettlementItem.settlement_id)
+ .join(BizOrder, BizOrder.id == OrdClientSettlementItem.order_id)
+ .filter(
+ OrdClientSettlementItem.is_del == 0,
+ BizOrder.is_del == 0,
+ BizOrder.region_id == region_id,
+ )
+ .distinct()
+ )
+ q = q.filter(OrdClientSettlement.id.in_(sub))
+ total = q.count()
+ rows = q.order_by(OrdClientSettlement.settlement_at.desc()).offset(skip).limit(limit).all()
+ return rows, total
+
+ def get_with_items(
+ self, db: Session, settlement_id: str
+ ) -> Tuple[Optional[OrdClientSettlement], List[OrdClientSettlementItem]]:
+ main = self.get(db, settlement_id)
+ if not main or main.is_del == 1:
+ return None, []
+ items = (
+ db.query(OrdClientSettlementItem)
+ .filter(
+ OrdClientSettlementItem.settlement_id == settlement_id,
+ OrdClientSettlementItem.is_del == 0,
+ )
+ .order_by(OrdClientSettlementItem.create_at.asc())
+ .all()
+ )
+ return main, items
+
+ def belongs_to_region(self, db: Session, settlement_id: str, region_id: str) -> bool:
+ if not region_id:
+ return True
+ return (
+ db.query(OrdClientSettlementItem.id)
+ .join(BizOrder, BizOrder.id == OrdClientSettlementItem.order_id)
+ .filter(
+ OrdClientSettlementItem.settlement_id == settlement_id,
+ OrdClientSettlementItem.is_del == 0,
+ BizOrder.is_del == 0,
+ BizOrder.region_id == region_id,
+ )
+ .first()
+ is not None
+ )
+
+ def update_with_history(
+ self,
+ db: Session,
+ settlement_id: str,
+ obj_in: dict,
+ updater: Optional[dict] = None,
+ ) -> Optional[OrdClientSettlement]:
+ original = self.get(db, settlement_id)
+ if not original or original.is_del == 1:
+ return None
+ his = OrdClientSettlementHis(**self._snapshot_main(original))
+ db.add(his)
+ return self.update(db, settlement_id, obj_in, updater)
+
+
+class OrdClientSettlementItemCRUD(CRUDBase[OrdClientSettlementItem]):
+ def _snapshot_item(self, row: OrdClientSettlementItem) -> Dict[str, Any]:
+ return {
+ "id": row.id,
+ "settlement_id": row.settlement_id,
+ "order_id": row.order_id,
+ "order_no": row.order_no,
+ "client_id": row.client_id,
+ "order_amount": row.order_amount,
+ "create_at": row.create_at,
+ "create_by": row.create_by,
+ "create_name": row.create_name,
+ "update_at": row.update_at,
+ "update_by": row.update_by,
+ }
+
+ def update_with_history(
+ self,
+ db: Session,
+ item_id: str,
+ obj_in: dict,
+ updater: Optional[dict] = None,
+ ) -> Optional[OrdClientSettlementItem]:
+ original = self.get(db, item_id)
+ if not original or original.is_del == 1:
+ return None
+ his = OrdClientSettlementItemHis(**self._snapshot_item(original))
+ db.add(his)
+ return self.update(db, item_id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, item_id: str, updater: Optional[dict] = None) -> bool:
+ original = self.get(db, item_id)
+ if not original or original.is_del == 1:
+ return False
+ his = OrdClientSettlementItemHis(**self._snapshot_item(original))
+ db.add(his)
+ return self.update(db, item_id, {"is_del": 1}, updater) is not None
+
+
+class OrdSupplierSettlementCRUD(CRUDBase[OrdSupplierSettlement]):
+ def _snapshot_main(self, row: OrdSupplierSettlement) -> Dict[str, Any]:
+ return {
+ "id": row.id,
+ "settlement_no": row.settlement_no,
+ "supplier_id": row.supplier_id,
+ "supplier_name": row.supplier_name,
+ "order_count": row.order_count,
+ "total_amount": row.total_amount,
+ "voucher_urls": row.voucher_urls,
+ "remark": row.remark,
+ "settlement_at": row.settlement_at,
+ "status": row.status,
+ "create_at": row.create_at,
+ "create_by": row.create_by,
+ "create_name": row.create_name,
+ "update_at": row.update_at,
+ "update_by": row.update_by,
+ }
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ *,
+ supplier_id: Optional[str] = None,
+ settlement_no: Optional[str] = None,
+ start_date: Optional[datetime] = None,
+ end_date: Optional[datetime] = None,
+ region_id: Optional[str] = None,
+ skip: int = 0,
+ limit: int = 10,
+ ) -> Tuple[List[OrdSupplierSettlement], int]:
+ q = db.query(OrdSupplierSettlement).filter(OrdSupplierSettlement.is_del == 0)
+ if supplier_id:
+ q = q.filter(OrdSupplierSettlement.supplier_id == supplier_id)
+ if settlement_no:
+ q = q.filter(OrdSupplierSettlement.settlement_no.like(f"%{settlement_no}%"))
+ if start_date:
+ q = q.filter(OrdSupplierSettlement.settlement_at >= start_date)
+ if end_date:
+ q = q.filter(OrdSupplierSettlement.settlement_at <= end_date)
+ if region_id:
+ sub = (
+ db.query(OrdSupplierSettlementItem.settlement_id)
+ .join(BizOrder, BizOrder.id == OrdSupplierSettlementItem.order_id)
+ .filter(
+ OrdSupplierSettlementItem.is_del == 0,
+ BizOrder.is_del == 0,
+ BizOrder.region_id == region_id,
+ )
+ .distinct()
+ )
+ q = q.filter(OrdSupplierSettlement.id.in_(sub))
+ total = q.count()
+ rows = q.order_by(OrdSupplierSettlement.settlement_at.desc()).offset(skip).limit(limit).all()
+ return rows, total
+
+ def get_with_items(
+ self, db: Session, settlement_id: str
+ ) -> Tuple[Optional[OrdSupplierSettlement], List[OrdSupplierSettlementItem]]:
+ main = self.get(db, settlement_id)
+ if not main or main.is_del == 1:
+ return None, []
+ items = (
+ db.query(OrdSupplierSettlementItem)
+ .filter(
+ OrdSupplierSettlementItem.settlement_id == settlement_id,
+ OrdSupplierSettlementItem.is_del == 0,
+ )
+ .order_by(OrdSupplierSettlementItem.create_at.asc())
+ .all()
+ )
+ return main, items
+
+ def belongs_to_region(self, db: Session, settlement_id: str, region_id: str) -> bool:
+ if not region_id:
+ return True
+ return (
+ db.query(OrdSupplierSettlementItem.id)
+ .join(BizOrder, BizOrder.id == OrdSupplierSettlementItem.order_id)
+ .filter(
+ OrdSupplierSettlementItem.settlement_id == settlement_id,
+ OrdSupplierSettlementItem.is_del == 0,
+ BizOrder.is_del == 0,
+ BizOrder.region_id == region_id,
+ )
+ .first()
+ is not None
+ )
+
+ def update_with_history(
+ self,
+ db: Session,
+ settlement_id: str,
+ obj_in: dict,
+ updater: Optional[dict] = None,
+ ) -> Optional[OrdSupplierSettlement]:
+ original = self.get(db, settlement_id)
+ if not original or original.is_del == 1:
+ return None
+ his = OrdSupplierSettlementHis(**self._snapshot_main(original))
+ db.add(his)
+ return self.update(db, settlement_id, obj_in, updater)
+
+
+class OrdSupplierSettlementItemCRUD(CRUDBase[OrdSupplierSettlementItem]):
+ def _snapshot_item(self, row: OrdSupplierSettlementItem) -> Dict[str, Any]:
+ return {
+ "id": row.id,
+ "settlement_id": row.settlement_id,
+ "order_id": row.order_id,
+ "order_no": row.order_no,
+ "supplier_id": row.supplier_id,
+ "order_amount": row.order_amount,
+ "create_at": row.create_at,
+ "create_by": row.create_by,
+ "create_name": row.create_name,
+ "update_at": row.update_at,
+ "update_by": row.update_by,
+ }
+
+ def update_with_history(
+ self,
+ db: Session,
+ item_id: str,
+ obj_in: dict,
+ updater: Optional[dict] = None,
+ ) -> Optional[OrdSupplierSettlementItem]:
+ original = self.get(db, item_id)
+ if not original or original.is_del == 1:
+ return None
+ his = OrdSupplierSettlementItemHis(**self._snapshot_item(original))
+ db.add(his)
+ return self.update(db, item_id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, item_id: str, updater: Optional[dict] = None) -> bool:
+ original = self.get(db, item_id)
+ if not original or original.is_del == 1:
+ return False
+ his = OrdSupplierSettlementItemHis(**self._snapshot_item(original))
+ db.add(his)
+ return self.update(db, item_id, {"is_del": 1}, updater) is not None
+
+
+ord_client_settlement_crud = OrdClientSettlementCRUD(OrdClientSettlement)
+ord_client_settlement_item_crud = OrdClientSettlementItemCRUD(OrdClientSettlementItem)
+ord_supplier_settlement_crud = OrdSupplierSettlementCRUD(OrdSupplierSettlement)
+ord_supplier_settlement_item_crud = OrdSupplierSettlementItemCRUD(OrdSupplierSettlementItem)
diff --git a/backend/app/crud/order.py b/backend/app/crud/order.py
new file mode 100644
index 00000000..0caf7741
--- /dev/null
+++ b/backend/app/crud/order.py
@@ -0,0 +1,1006 @@
+"""订单CRUD"""
+from typing import Optional, List
+import json
+from datetime import datetime
+from decimal import Decimal
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, or_, func, select, exists
+
+from app.models.models import BizOrder, BizOrderItem, BizOrderStatusLog, BizOrderHis, BizOrderAdjust, SysUser
+from app.crud.base import CRUDBase
+from app.core.utils import generate_id
+from app.core.order_status_compat import derive_three_status_from_legacy, derive_legacy_status_from_three
+
+
+class OrderCRUD(CRUDBase[BizOrder]):
+ @staticmethod
+ def normalize_payment_method(payment_method: Optional[str]) -> str:
+ """统一支付方式值,仅保留 online/offline。"""
+ method = (payment_method or "").strip().lower()
+ if method in ("wechat", "wx", "online"):
+ return "online"
+ if method == "offline":
+ return "offline"
+ return "online"
+
+ def sync_pay_fields_from_amounts(self, order: BizOrder) -> None:
+ """根据已付/应收同步 pay_status、payment_time 与兼容字段 status(付清后才是 paid)。"""
+ pa = order.paid_amount or Decimal("0")
+ ra = order.receivable_amount or Decimal("0")
+ if ra > 0 and pa >= ra:
+ order.pay_status = "paid"
+ if not order.payment_time:
+ order.payment_time = datetime.now()
+ else:
+ order.pay_status = "pending_payment"
+ order.status = derive_legacy_status_from_three(
+ order_status=getattr(order, "order_status", None) or "pending_confirm",
+ delivery_status=getattr(order, "delivery_status", None) or "pending_dispatch",
+ pay_status=order.pay_status,
+ )
+
+ """订单CRUD操作"""
+
+ def _sync_three_status_fields_from_legacy(self, order: BizOrder):
+ """阶段A:保持三字段与旧 status 一致(先兼容,再逐步迁移逻辑)。"""
+ ts = derive_three_status_from_legacy(order.status, paid_amount=order.paid_amount)
+ order.order_status = ts.order_status
+ order.delivery_status = ts.delivery_status
+ order.pay_status = ts.pay_status
+
+ def _apply_three_status(self, order: BizOrder, *, order_status: str = None, delivery_status: str = None, pay_status: str = None):
+ """阶段D收尾:优先写三字段,再回写兼容 status。"""
+ if order_status is not None:
+ order.order_status = order_status
+ if delivery_status is not None:
+ order.delivery_status = delivery_status
+ if pay_status is not None:
+ order.pay_status = pay_status
+ order.status = derive_legacy_status_from_three(
+ order_status=order.order_status,
+ delivery_status=order.delivery_status,
+ pay_status=order.pay_status,
+ )
+
+ def get_by_client(self, db: Session, client_id: str) -> List[BizOrder]:
+ """根据客户获取订单列表"""
+ return db.query(BizOrder).filter(
+ and_(
+ BizOrder.client_id == client_id,
+ BizOrder.is_del == 0
+ )
+ ).order_by(BizOrder.create_at.desc()).all()
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ client_id: str = None,
+ region_id: str = None,
+ status: str = None,
+ order_status: str = None,
+ delivery_status: str = None,
+ pay_status: str = None,
+ payment_method: str = None,
+ cancel_status: str = None,
+ order_no: str = None,
+ keyword: str = None,
+ start_date: str = None,
+ end_date: str = None,
+ skip: int = 0,
+ limit: int = 10,
+ list_tab: str = None,
+ ) -> tuple[List[BizOrder], int]:
+ """多条件查询订单"""
+ query = db.query(BizOrder).filter(BizOrder.is_del == 0)
+
+ if client_id:
+ query = query.filter(BizOrder.client_id == client_id)
+ if region_id:
+ query = query.filter(BizOrder.region_id == region_id)
+ if status:
+ query = query.filter(BizOrder.status == status)
+ if order_status:
+ query = query.filter(BizOrder.order_status == order_status)
+ if delivery_status:
+ query = query.filter(BizOrder.delivery_status == delivery_status)
+ if pay_status:
+ query = query.filter(BizOrder.pay_status == pay_status)
+ if payment_method:
+ pm = self.normalize_payment_method(payment_method)
+ if pm == "offline":
+ query = query.filter(BizOrder.payment_method == "offline")
+ else:
+ query = query.filter(
+ or_(
+ BizOrder.payment_method == "online",
+ BizOrder.payment_method.is_(None),
+ BizOrder.payment_method == "",
+ )
+ )
+ if cancel_status:
+ query = query.filter(BizOrder.cancel_status == cancel_status)
+ if order_no:
+ query = query.filter(BizOrder.order_no.like(f"%{order_no}%"))
+ if keyword:
+ kw = (keyword or "").strip()
+ if kw:
+ pattern = f"%{kw}%"
+ client_user_match = exists(
+ select(1).select_from(SysUser).where(
+ and_(
+ SysUser.id == BizOrder.client_id,
+ SysUser.is_del == 0,
+ or_(
+ SysUser.real_name.like(pattern),
+ SysUser.username.like(pattern),
+ SysUser.phone.like(pattern),
+ ),
+ )
+ )
+ )
+ query = query.filter(
+ or_(
+ BizOrder.order_no.like(pattern),
+ BizOrder.contact_person.like(pattern),
+ BizOrder.contact_phone.like(pattern),
+ client_user_match,
+ )
+ )
+ if start_date:
+ query = query.filter(BizOrder.create_at >= start_date)
+ if end_date:
+ query = query.filter(BizOrder.create_at <= end_date)
+
+ # 客户端订单列表 Tab(与小程序 order.vue 分段一致,保证分页有效)
+ if list_tab == "pending_payment":
+ query = query.filter(
+ and_(
+ BizOrder.order_status == "pending_payment",
+ BizOrder.pay_status == "pending_payment",
+ BizOrder.cancel_status != "pending_review",
+ )
+ )
+ elif list_tab == "pending_confirm":
+ query = query.filter(
+ and_(
+ BizOrder.order_status == "pending_confirm",
+ BizOrder.cancel_status != "pending_review",
+ )
+ )
+ elif list_tab == "completed":
+ query = query.filter(BizOrder.order_status == "completed")
+ elif list_tab == "cancelled":
+ query = query.filter(BizOrder.order_status == "cancelled")
+
+ total = query.count()
+ orders = query.order_by(BizOrder.create_at.desc()).offset(skip).limit(limit).all()
+ return orders, total
+
+ def get_by_order_no(self, db: Session, order_no: str) -> Optional[BizOrder]:
+ """根据订单号获取订单"""
+ return db.query(BizOrder).filter(
+ and_(
+ BizOrder.order_no == order_no,
+ BizOrder.is_del == 0
+ )
+ ).first()
+
+ def create_order(
+ self,
+ db: Session,
+ client_id: str,
+ region_id: str,
+ price_list_id: str,
+ price_version: int,
+ items: List[dict],
+ address_id: str,
+ address_info: dict,
+ payment_method: str,
+ remark: str = None,
+ creator: dict = None
+ ) -> BizOrder:
+ """创建订单"""
+ # 生成订单号
+ order_no = self._generate_order_no(db)
+
+ # 计算订单总金额
+ total_amount = sum(
+ Decimal(str(item.get("quantity", 0))) * Decimal(str(item.get("unitPrice", 0)))
+ for item in items
+ )
+
+ normalized_payment_method = self.normalize_payment_method(payment_method)
+
+ # 线上:未支付前主状态为待支付;线下:未收款前 pay_status 仍为待支付,但主状态进入待收货/履约阶段,可与 pending_dispatch 一并参与分配配送
+ if normalized_payment_method == "offline":
+ init_order_status = "pending_confirm"
+ else:
+ init_order_status = "pending_payment"
+ init_delivery_status = "pending_dispatch"
+ init_pay_status = "pending_payment"
+ order = BizOrder(
+ id=generate_id(),
+ order_no=order_no,
+ region_id=region_id,
+ client_id=client_id,
+ price_list_id=price_list_id,
+ price_version=price_version,
+ status=derive_legacy_status_from_three(
+ order_status=init_order_status,
+ delivery_status=init_delivery_status,
+ pay_status=init_pay_status,
+ ),
+ order_status=init_order_status,
+ delivery_status=init_delivery_status,
+ pay_status=init_pay_status,
+ cancel_status="none",
+ payment_method=normalized_payment_method,
+ client_settlement_status="settled" if normalized_payment_method == "online" else "pending",
+ supplier_settlement_status="pending",
+ client_settlement_at=datetime.now() if normalized_payment_method == "online" else None,
+ supplier_settlement_at=None,
+ total_amount=total_amount,
+ paid_amount=Decimal("0"),
+ receivable_amount=total_amount,
+ refund_amount=Decimal("0"),
+ contact_person=address_info.get("contactPerson"),
+ contact_phone=address_info.get("contactPhone"),
+ delivery_address=address_info.get("fullAddress"),
+ remark=remark,
+ payment_time=None,
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(order)
+ db.flush()
+
+ # 创建订单项
+ for item in items:
+ amount = Decimal(str(item.get("quantity", 0))) * Decimal(str(item.get("unitPrice", 0)))
+ order_item = BizOrderItem(
+ id=generate_id(),
+ order_id=order.id,
+ product_id=item.get("productId"),
+ unit_id=item.get("unitId"),
+ quantity=Decimal(str(item.get("quantity", 0))),
+ unit_price=Decimal(str(item.get("unitPrice", 0))),
+ amount=amount,
+ delivered_quantity=Decimal("0"),
+ status="normal",
+ note=item.get("note"),
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(order_item)
+
+ # 创建订单状态日志
+ self._create_status_log(
+ db,
+ order.id,
+ None,
+ order.status,
+ creator.get("user_id") if creator else client_id,
+ "client",
+ "创建订单"
+ )
+
+ db.commit()
+ db.refresh(order)
+ return order
+
+ def update_status(
+ self,
+ db: Session,
+ order_id: str,
+ new_status: str,
+ operator_id: str,
+ operator_role: str,
+ reason: str = None,
+ note: str = None
+ ) -> Optional[BizOrder]:
+ """更新订单状态"""
+ order = self.get(db, order_id)
+ if not order:
+ return None
+
+ old_status = order.status
+ order.status = new_status
+ order.update_at = datetime.now()
+
+ # 特殊状态处理
+ if new_status == "paid":
+ order.payment_time = datetime.now()
+ elif new_status == "completed":
+ order.completed_time = datetime.now()
+ order.completed_by = operator_id
+
+ # 阶段A:同步三字段
+ self._sync_three_status_fields_from_legacy(order)
+
+ # 创建状态变更日志
+ self._create_status_log(
+ db,
+ order_id,
+ old_status,
+ new_status,
+ operator_id,
+ operator_role,
+ reason,
+ note
+ )
+
+ db.commit()
+ db.refresh(order)
+ return order
+
+ def cancel_order(
+ self,
+ db: Session,
+ order_id: str,
+ cancel_type: str,
+ reason: str,
+ operator_id: str,
+ operator_role: str
+ ) -> Optional[BizOrder]:
+ """取消订单"""
+ order = self.get(db, order_id)
+ if not order:
+ return None
+
+ # 待支付订单直接取消
+ if order.status == "pending_payment":
+ return self.update_status(
+ db,
+ order_id,
+ "cancelled",
+ operator_id,
+ operator_role,
+ reason=reason
+ )
+
+ # 已支付订单申请取消
+ if order.delivery_status in ["pending_dispatch", "delivering"]:
+ self._apply_three_status(order, order_status="pending_confirm")
+ order.cancel_status = "pending_review"
+ order.cancel_reason = reason
+ order.update_at = datetime.now()
+
+ self._create_status_log(
+ db,
+ order_id,
+ order.status,
+ "pending_review_cancel", # 兼容历史日志标识
+ operator_id,
+ operator_role,
+ reason=reason
+ )
+
+ db.commit()
+ db.refresh(order)
+ return order
+
+ return None
+
+ def review_cancel(
+ self,
+ db: Session,
+ order_id: str,
+ approved: bool,
+ reviewer_id: str,
+ review_note: str = None
+ ) -> Optional[BizOrder]:
+ """审核取消订单"""
+ order = self.get(db, order_id)
+ if not order or order.cancel_status != "pending_review":
+ return None
+
+ if approved:
+ order.cancel_status = "approved"
+ self._apply_three_status(order, order_status="cancelled")
+ new_status = order.status
+ else:
+ # 从状态日志获取取消前的状态
+ from app.models.models import BizOrderStatusLog
+ cancel_log = db.query(BizOrderStatusLog).filter(
+ BizOrderStatusLog.order_id == order_id,
+ BizOrderStatusLog.to_status == "pending_review_cancel"
+ ).order_by(BizOrderStatusLog.create_at.desc()).first()
+
+ # 恢复为取消前的状态(pending_delivery, delivering, partial_delivered)
+ if cancel_log and cancel_log.from_status:
+ # 兼容历史:尽量从旧状态恢复到三字段
+ order.status = cancel_log.from_status
+ self._sync_three_status_fields_from_legacy(order)
+ else:
+ self._apply_three_status(order, order_status="pending_confirm", delivery_status="pending_dispatch")
+ order.cancel_status = "rejected"
+ new_status = order.status
+
+ order.cancel_review_time = datetime.now()
+ order.cancel_reviewed_by = reviewer_id
+ order.cancel_review_note = review_note
+ order.update_at = datetime.now()
+
+ self._create_status_log(
+ db,
+ order_id,
+ "pending_review_cancel",
+ new_status,
+ reviewer_id,
+ "admin",
+ reason=f"审核{'通过' if approved else '拒绝'}: {review_note}"
+ )
+
+ db.commit()
+ db.refresh(order)
+ return order
+
+ def confirm_receive(
+ self,
+ db: Session,
+ order_id: str,
+ operator_id: str
+ ) -> Optional[BizOrder]:
+ """确认收货"""
+ order = self.get(db, order_id)
+ if not order:
+ return None
+
+ # 仅待确认收货状态支持确认收货
+ if order.status == "pending_confirm":
+ return self.update_status(
+ db,
+ order_id,
+ "completed",
+ operator_id,
+ "client",
+ reason="客户确认收货"
+ )
+ return None
+
+ def auto_confirm_receive_overdue(
+ self,
+ db: Session,
+ *,
+ before_threshold: datetime,
+ configured_days: int,
+ ) -> int:
+ """送达后超过 configured_days 未确认收货的订单,系统自动确认(与 force_complete 一致写三字段与日志)。"""
+ threshold_expr = func.coalesce(BizOrder.delivered_time, BizOrder.update_at)
+ orders = (
+ db.query(BizOrder)
+ .filter(
+ BizOrder.is_del == 0,
+ BizOrder.order_status == "pending_confirm",
+ BizOrder.delivery_status == "delivered",
+ BizOrder.cancel_status.in_(("none", "rejected")),
+ threshold_expr <= before_threshold,
+ )
+ .all()
+ )
+ if not orders:
+ return 0
+ note = f"送达后超过{configured_days}天未确认收货,系统自动确认"
+ count = 0
+ for order in orders:
+ old_status = order.status
+ self._apply_three_status(order, order_status="completed")
+ order.completed_time = datetime.now()
+ order.completed_by = "system"
+ order.update_at = datetime.now()
+ self._create_status_log(
+ db,
+ order.id,
+ old_status,
+ "completed",
+ "system",
+ "system",
+ reason=note,
+ )
+ count += 1
+ db.commit()
+ return count
+
+ def _generate_order_no(self, db: Session) -> str:
+ """生成订单号"""
+ today = datetime.now().strftime("%Y%m%d")
+ prefix = f"ORD{today}"
+
+ # 查询当天最大订单号
+ last_order = db.query(BizOrder).filter(
+ BizOrder.order_no.like(f"{prefix}%")
+ ).order_by(BizOrder.order_no.desc()).first()
+
+ if last_order:
+ last_no = int(last_order.order_no[-4:])
+ new_no = last_no + 1
+ else:
+ new_no = 1
+
+ return f"{prefix}{str(new_no).zfill(4)}"
+
+ def _create_status_log(
+ self,
+ db: Session,
+ order_id: str,
+ from_status: str,
+ to_status: str,
+ operator_id: str,
+ operator_role: str,
+ reason: str = None,
+ note: str = None,
+ create_name: str = None,
+ log_create_at: Optional[datetime] = None,
+ ):
+ """创建订单状态变更日志"""
+ ts = log_create_at or datetime.now()
+ log = BizOrderStatusLog(
+ id=generate_id(),
+ order_id=order_id,
+ from_status=from_status,
+ to_status=to_status,
+ operator_id=operator_id,
+ operator_role=operator_role,
+ reason=reason,
+ note=note,
+ create_by=operator_id,
+ create_name=create_name or None,
+ create_at=ts,
+ update_at=ts,
+ )
+ db.add(log)
+
+ def close_unpaid_online_orders(
+ self,
+ db: Session,
+ *,
+ before_time: datetime,
+ timeout_note: str,
+ ) -> int:
+ """关闭超时未支付的线上订单。"""
+ candidates = db.query(BizOrder).filter(
+ BizOrder.is_del == 0,
+ BizOrder.payment_method == "online",
+ BizOrder.pay_status == "pending_payment",
+ BizOrder.order_status == "pending_payment",
+ BizOrder.create_at <= before_time,
+ ).all()
+ count = 0
+ for order in candidates:
+ self._apply_three_status(order, order_status="cancelled", delivery_status="pending_dispatch", pay_status="pending_payment")
+ order.cancel_status = "approved"
+ order.cancel_reason = "线上支付超时自动关闭"
+ order.cancel_review_note = "系统自动关闭"
+ order.update_at = datetime.now()
+ self._create_status_log(
+ db,
+ order.id,
+ "pending_payment",
+ "cancelled",
+ "system",
+ "system",
+ reason="线上支付超时自动关闭",
+ note=timeout_note,
+ )
+ count += 1
+ if count:
+ db.commit()
+ return count
+
+ def admin_cancel(
+ self,
+ db: Session,
+ *,
+ order_id: str,
+ operator_id: str,
+ operator_name: str,
+ reason: str,
+ note: Optional[str] = None,
+ evidence_image_urls: Optional[List[str]] = None,
+ ) -> Optional[BizOrder]:
+ """运营取消订单。"""
+ order = self.get(db, order_id)
+ if not order:
+ return None
+ old_status = order.status
+ self._apply_three_status(order, order_status="cancelled")
+ order.cancel_status = "approved"
+ order.cancel_reason = reason
+ if note:
+ order.cancel_review_note = note
+ order.cancel_review_time = datetime.now()
+ order.cancel_reviewed_by = operator_id
+ order.update_at = datetime.now()
+ self._create_status_log(
+ db,
+ order.id,
+ old_status,
+ "cancelled",
+ operator_id,
+ "admin",
+ reason=reason,
+ note=json.dumps({"note": note or "", "evidenceImageUrls": evidence_image_urls or []}, ensure_ascii=False),
+ )
+ db.commit()
+ db.refresh(order)
+ return order
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizOrder]:
+ """更新订单并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "order_no": original.order_no,
+ "region_id": original.region_id,
+ "client_id": original.client_id,
+ "price_list_id": original.price_list_id,
+ "price_version": original.price_version,
+ "status": original.status,
+ "order_status": getattr(original, "order_status", None),
+ "delivery_status": getattr(original, "delivery_status", None),
+ "pay_status": getattr(original, "pay_status", None),
+ "cancel_status": getattr(original, "cancel_status", None),
+ "payment_method": original.payment_method,
+ "client_settlement_status": getattr(original, "client_settlement_status", "pending"),
+ "supplier_settlement_status": getattr(original, "supplier_settlement_status", "pending"),
+ "client_settlement_at": getattr(original, "client_settlement_at", None),
+ "supplier_settlement_at": getattr(original, "supplier_settlement_at", None),
+ "total_amount": original.total_amount,
+ "paid_amount": original.paid_amount,
+ "receivable_amount": original.receivable_amount,
+ "refund_amount": original.refund_amount,
+ "contact_person": original.contact_person,
+ "contact_phone": original.contact_phone,
+ "delivery_address": original.delivery_address,
+ "cancel_reason": original.cancel_reason,
+ "cancel_review_time": original.cancel_review_time,
+ "cancel_reviewed_by": original.cancel_reviewed_by,
+ "cancel_review_note": original.cancel_review_note,
+ "completed_time": original.completed_time,
+ "completed_by": original.completed_by,
+ "remark": original.remark,
+ "payment_time": original.payment_time,
+ "delivery_assign_time": original.delivery_assign_time,
+ "delivered_time": getattr(original, "delivered_time", None),
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizOrderHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, id: str) -> bool:
+ """删除订单并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return False
+
+ his_data = {
+ "id": original.id,
+ "order_no": original.order_no,
+ "region_id": original.region_id,
+ "client_id": original.client_id,
+ "price_list_id": original.price_list_id,
+ "price_version": original.price_version,
+ "status": original.status,
+ "order_status": getattr(original, "order_status", None),
+ "delivery_status": getattr(original, "delivery_status", None),
+ "pay_status": getattr(original, "pay_status", None),
+ "cancel_status": getattr(original, "cancel_status", None),
+ "payment_method": original.payment_method,
+ "client_settlement_status": getattr(original, "client_settlement_status", "pending"),
+ "supplier_settlement_status": getattr(original, "supplier_settlement_status", "pending"),
+ "client_settlement_at": getattr(original, "client_settlement_at", None),
+ "supplier_settlement_at": getattr(original, "supplier_settlement_at", None),
+ "total_amount": original.total_amount,
+ "paid_amount": original.paid_amount,
+ "receivable_amount": original.receivable_amount,
+ "refund_amount": original.refund_amount,
+ "contact_person": original.contact_person,
+ "contact_phone": original.contact_phone,
+ "delivery_address": original.delivery_address,
+ "delivered_time": getattr(original, "delivered_time", None),
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizOrderHis(**his_data)
+ db.add(his)
+
+ return self.delete(db, id)
+
+ def lock_order(
+ self,
+ db: Session,
+ order_id: str,
+ operator_id: str
+ ) -> Optional[BizOrder]:
+ """锁定订单,防止并发操作"""
+ order = self.get(db, order_id)
+ if not order:
+ return None
+
+ # 如果已被其他操作员锁定,不能重复锁定
+ if order.lock_operator_id and order.lock_operator_id != operator_id:
+ lock_time = order.lock_time.strftime("%Y-%m-%d %H:%M:%S") if order.lock_time else "未知"
+ raise ValueError(f"订单已被用户 {order.lock_operator_id} 于 {lock_time} 锁定")
+
+ order.lock_operator_id = operator_id
+ order.lock_time = datetime.now()
+ order.update_at = datetime.now()
+
+ db.commit()
+ db.refresh(order)
+ return order
+
+ def unlock_order(
+ self,
+ db: Session,
+ order_id: str,
+ operator_id: str
+ ) -> Optional[BizOrder]:
+ """解锁订单"""
+ order = self.get(db, order_id)
+ if not order:
+ return None
+
+ # 只能解锁自己锁定的订单
+ if order.lock_operator_id and order.lock_operator_id != operator_id:
+ raise ValueError("只能解锁自己锁定的订单")
+
+ order.lock_operator_id = None
+ order.lock_time = None
+ order.update_at = datetime.now()
+
+ db.commit()
+ db.refresh(order)
+ return order
+
+ def force_complete(
+ self,
+ db: Session,
+ order_id: str,
+ operator_id: str,
+ operator_role: str,
+ reason: str = None
+ ) -> Optional[BizOrder]:
+ """运营强制完结订单"""
+ order = self.get(db, order_id)
+ if not order:
+ return None
+
+ # 只有待确认收货状态可以强制完结
+ if order.order_status not in ["pending_confirm"]:
+ raise ValueError("只有待确认收货状态的订单可以强制完结")
+
+ old_status = order.status
+ self._apply_three_status(order, order_status="completed")
+ order.completed_time = datetime.now()
+ order.completed_by = operator_id
+ order.update_at = datetime.now()
+
+ # 记录状态变更
+ self._create_status_log(
+ db,
+ order_id,
+ old_status,
+ "completed",
+ operator_id,
+ operator_role,
+ reason=reason or "运营强制完结"
+ )
+
+ db.commit()
+ db.refresh(order)
+ return order
+
+ def update_to_partial_delivered(
+ self,
+ db: Session,
+ order_id: str,
+ operator_id: str,
+ operator_role: str,
+ reason: str = None
+ ) -> Optional[BizOrder]:
+ """已废弃:旧的部分送达流程不再支持。"""
+ raise ValueError("部分送达流程已下线")
+
+ def update_to_confirm(
+ self,
+ db: Session,
+ order_id: str,
+ operator_id: str,
+ operator_role: str,
+ reason: str = None
+ ) -> Optional[BizOrder]:
+ """已废弃:旧的部分送达确认流程不再支持。"""
+ raise ValueError("部分送达确认流程已下线")
+
+
+class OrderItemCRUD(CRUDBase[BizOrderItem]):
+ """订单项CRUD操作"""
+
+ def get_by_order(self, db: Session, order_id: str) -> List[BizOrderItem]:
+ """获取订单的所有订单项"""
+ return db.query(BizOrderItem).filter(
+ and_(
+ BizOrderItem.order_id == order_id,
+ BizOrderItem.is_del == 0
+ )
+ ).all()
+
+ def get_count_by_order(self, db: Session, order_id: str) -> int:
+ """获取订单项数量"""
+ return db.query(BizOrderItem).filter(
+ and_(
+ BizOrderItem.order_id == order_id,
+ BizOrderItem.is_del == 0
+ )
+ ).count()
+
+ def update_item_adjustment(
+ self,
+ db: Session,
+ item_id: str,
+ new_quantity: Decimal = None,
+ new_amount: Decimal = None,
+ new_status: str = None,
+ note: str = None,
+ updater_id: str = None,
+ updater_name: str = None
+ ) -> Optional[BizOrderItem]:
+ """更新订单项(调整后)"""
+ item = self.get(db, item_id)
+ if not item:
+ return None
+
+ if new_quantity is not None:
+ item.quantity = new_quantity
+ if new_amount is not None:
+ item.amount = new_amount
+ if new_status is not None:
+ item.status = new_status
+ if note is not None:
+ item.note = note
+
+ item.update_at = datetime.now()
+ item.update_by = updater_id
+
+ db.commit()
+ db.refresh(item)
+ return item
+
+ def mark_as_reduced(
+ self,
+ db: Session,
+ item_id: str,
+ new_quantity: Decimal,
+ new_amount: Decimal,
+ note: str = None
+ ) -> Optional[BizOrderItem]:
+ """标记为已减量"""
+ return self.update_item_adjustment(
+ db, item_id,
+ new_quantity=new_quantity,
+ new_amount=new_amount,
+ new_status="reduced",
+ note=note
+ )
+
+ def mark_as_deleted(
+ self,
+ db: Session,
+ item_id: str,
+ note: str = None
+ ) -> Optional[BizOrderItem]:
+ """标记为已删除"""
+ return self.update_item_adjustment(
+ db, item_id,
+ new_status="deleted",
+ note=note
+ )
+
+ def mark_as_substituted(
+ self,
+ db: Session,
+ item_id: str,
+ new_product_id: str,
+ new_quantity: Decimal,
+ new_amount: Decimal,
+ note: str = None
+ ) -> Optional[BizOrderItem]:
+ """标记为已替代"""
+ item = self.update_item_adjustment(
+ db, item_id,
+ new_status="substituted",
+ note=note
+ )
+ return item
+
+
+class OrderStatusLogCRUD(CRUDBase[BizOrderStatusLog]):
+ """订单状态日志CRUD操作"""
+
+ def get_by_order(self, db: Session, order_id: str) -> List[BizOrderStatusLog]:
+ """获取订单的状态变更日志"""
+ return db.query(BizOrderStatusLog).filter(
+ BizOrderStatusLog.order_id == order_id
+ ).order_by(BizOrderStatusLog.create_at.asc()).all()
+
+
+class OrderAdjustCRUD(CRUDBase[BizOrderAdjust]):
+ """订单调整记录CRUD操作"""
+
+ def get_by_order(self, db: Session, order_id: str) -> List[BizOrderAdjust]:
+ """获取订单的所有调整记录"""
+ return db.query(BizOrderAdjust).filter(
+ and_(
+ BizOrderAdjust.order_id == order_id,
+ BizOrderAdjust.is_del == 0
+ )
+ ).order_by(BizOrderAdjust.create_at.desc()).all()
+
+ def get_by_order_item(self, db: Session, order_item_id: str) -> List[BizOrderAdjust]:
+ """获取订单项的调整记录"""
+ return db.query(BizOrderAdjust).filter(
+ and_(
+ BizOrderAdjust.order_item_id == order_item_id,
+ BizOrderAdjust.is_del == 0
+ )
+ ).order_by(BizOrderAdjust.create_at.desc()).all()
+
+ def create_adjust_record(
+ self,
+ db: Session,
+ order_id: str,
+ adjust_type: str,
+ operator_id: str,
+ operator_name: str,
+ order_item_id: str = None,
+ before_quantity: Decimal = None,
+ after_quantity: Decimal = None,
+ before_amount: Decimal = None,
+ after_amount: Decimal = None,
+ difference_amount: Decimal = None,
+ reason: str = None,
+ substitute_product_id: str = None,
+ snapshot_items_json: Optional[str] = None,
+ ) -> BizOrderAdjust:
+ """创建调整记录"""
+ adjust_record = BizOrderAdjust(
+ id=generate_id(),
+ order_id=order_id,
+ order_item_id=order_item_id,
+ adjust_type=adjust_type,
+ before_quantity=before_quantity,
+ after_quantity=after_quantity,
+ before_amount=before_amount,
+ after_amount=after_amount,
+ difference_amount=difference_amount or Decimal("0"),
+ reason=reason,
+ substitute_product_id=substitute_product_id,
+ snapshot_items_json=snapshot_items_json,
+ operator_id=operator_id,
+ operator_name=operator_name,
+ create_by=operator_id,
+ create_name=operator_name
+ )
+ db.add(adjust_record)
+ db.commit()
+ db.refresh(adjust_record)
+ return adjust_record
+
+
+order_crud = OrderCRUD(BizOrder)
+order_item_crud = OrderItemCRUD(BizOrderItem)
+order_status_log_crud = OrderStatusLogCRUD(BizOrderStatusLog)
+order_adjust_crud = OrderAdjustCRUD(BizOrderAdjust)
diff --git a/backend/app/crud/order_adjust.py b/backend/app/crud/order_adjust.py
new file mode 100644
index 00000000..0b934718
--- /dev/null
+++ b/backend/app/crud/order_adjust.py
@@ -0,0 +1,148 @@
+"""订单调整记录 CRUD"""
+from typing import Optional, List, Dict, Any
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from app.core.utils import generate_id
+from app.core.database import Base
+from app.crud.base import CRUDBase
+from app.models.models import BizOrderAdjust, BizOrderAdjustHis
+
+
+class OrderAdjustCRUD(CRUDBase[BizOrderAdjust]):
+ """订单调整记录CRUD"""
+
+ def __init__(self):
+ super().__init__(BizOrderAdjust)
+
+ def create_adjust(
+ self,
+ db: Session,
+ order_id: str,
+ order_item_id: str,
+ adjust_type: str,
+ before_quantity: float,
+ after_quantity: float,
+ before_amount: float,
+ after_amount: float,
+ reason: str,
+ operator_id: str,
+ operator_name: str,
+ substitute_product_id: str = None,
+ create_by: str = None,
+ create_name: str = None
+ ) -> BizOrderAdjust:
+ """创建订单调整记录"""
+ data = {
+ "id": generate_id(),
+ "order_id": order_id,
+ "order_item_id": order_item_id,
+ "adjust_type": adjust_type,
+ "before_quantity": before_quantity,
+ "after_quantity": after_quantity,
+ "before_amount": before_amount,
+ "after_amount": after_amount,
+ "difference_amount": before_amount - after_amount,
+ "reason": reason,
+ "operator_id": operator_id,
+ "operator_name": operator_name,
+ "substitute_product_id": substitute_product_id,
+ "create_by": create_by or operator_id,
+ "create_name": create_name or operator_name
+ }
+ return self.create(db, data)
+
+ def get_by_order(self, db: Session, order_id: str) -> List[BizOrderAdjust]:
+ """获取订单的所有调整记录"""
+ return db.query(self.model).filter(
+ and_(
+ self.model.order_id == order_id,
+ self.model.is_del == 0
+ )
+ ).order_by(self.model.create_at.desc()).all()
+
+ def get_by_order_item(self, db: Session, order_item_id: str) -> List[BizOrderAdjust]:
+ """获取订单项的所有调整记录"""
+ return db.query(self.model).filter(
+ and_(
+ self.model.order_item_id == order_item_id,
+ self.model.is_del == 0
+ )
+ ).order_by(self.model.create_at.desc()).all()
+
+ def get_total_adjust_amount(self, db: Session, order_id: str) -> float:
+ """获取订单的总调整金额(负数表示退款)"""
+ records = self.get_by_order(db, order_id)
+ return sum(r.difference_amount or 0 for r in records)
+
+ def get_list(
+ self,
+ db: Session,
+ page: int = 1,
+ pageSize: int = 10,
+ keyword: Optional[str] = None,
+ adjust_type: Optional[str] = None,
+ start_date: Optional[str] = None,
+ end_date: Optional[str] = None
+ ) -> tuple:
+ """获取订单调整列表(支持分页和搜索)"""
+ query = db.query(self.model).filter(self.model.is_del == 0)
+
+ # 关键词搜索(搜索订单号)
+ if keyword:
+ query = query.filter(self.model.order_id.like(f"%{keyword}%"))
+
+ # 调整类型筛选
+ if adjust_type:
+ query = query.filter(self.model.adjust_type == adjust_type)
+
+ # 日期范围筛选
+ if start_date:
+ query = query.filter(self.model.create_at >= start_date)
+ if end_date:
+ query = query.filter(self.model.create_at <= f"{end_date} 23:59:59")
+
+ # 获取总数
+ total = query.count()
+
+ # 分页
+ offset = (page - 1) * pageSize
+ records = query.order_by(self.model.create_at.desc()).offset(offset).limit(pageSize).all()
+
+ return records, total
+
+ def create_adjust_history(
+ self,
+ db: Session,
+ original_record: BizOrderAdjust,
+ operator_id: str,
+ operator_name: str
+ ) -> BizOrderAdjustHis:
+ """创建订单调整历史记录"""
+ from app.crud.base import HistoryCRUD
+
+ history_crud = HistoryCRUD(BizOrderAdjustHis)
+
+ data = {
+ "id": generate_id(),
+ "order_id": original_record.order_id,
+ "order_item_id": original_record.order_item_id,
+ "adjust_type": original_record.adjust_type,
+ "before_quantity": original_record.before_quantity,
+ "after_quantity": original_record.after_quantity,
+ "before_amount": original_record.before_amount,
+ "after_amount": original_record.after_amount,
+ "difference_amount": original_record.difference_amount,
+ "reason": original_record.reason,
+ "substitute_product_id": original_record.substitute_product_id,
+ "operator_id": operator_id,
+ "operator_name": operator_name,
+ "create_by": original_record.create_by,
+ "create_name": original_record.create_name
+ }
+
+ return history_crud.create(db, data)
+
+
+# 全局实例
+order_adjust_crud = OrderAdjustCRUD()
diff --git a/backend/app/crud/password_history.py b/backend/app/crud/password_history.py
new file mode 100644
index 00000000..8af00f7a
--- /dev/null
+++ b/backend/app/crud/password_history.py
@@ -0,0 +1,44 @@
+"""密码历史CRUD"""
+from typing import Optional, List
+from sqlalchemy.orm import Session
+
+from app.models.models import SysPasswordHistory
+from app.crud.base import CRUDBase
+from app.core.utils import generate_id
+
+
+class PasswordHistoryCRUD(CRUDBase[SysPasswordHistory]):
+ """密码历史CRUD操作"""
+
+ def create_password_history(
+ self,
+ db: Session,
+ user_id: str,
+ operation_type: str,
+ operator_id: str = None,
+ operator_name: str = None,
+ ip_address: str = None
+ ) -> SysPasswordHistory:
+ """创建密码修改历史"""
+ history = SysPasswordHistory(
+ id=generate_id(),
+ user_id=user_id,
+ operation_type=operation_type,
+ operator_id=operator_id,
+ operator_name=operator_name,
+ ip_address=ip_address
+ )
+ db.add(history)
+ db.commit()
+ db.refresh(history)
+ return history
+
+ def get_user_history(self, db: Session, user_id: str, limit: int = 5) -> List[SysPasswordHistory]:
+ """获取用户密码修改历史"""
+ return db.query(SysPasswordHistory).filter(
+ SysPasswordHistory.user_id == user_id,
+ SysPasswordHistory.is_del == 0
+ ).order_by(SysPasswordHistory.create_at.desc()).limit(limit).all()
+
+
+password_history_crud = PasswordHistoryCRUD(SysPasswordHistory)
diff --git a/backend/app/crud/price_list.py b/backend/app/crud/price_list.py
new file mode 100644
index 00000000..72e41821
--- /dev/null
+++ b/backend/app/crud/price_list.py
@@ -0,0 +1,387 @@
+"""价格清单CRUD"""
+from typing import Optional, List
+from datetime import datetime
+from decimal import Decimal
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, func
+
+from app.models.models import BizPriceList, BizPriceListItem, BizPriceListHis
+from app.crud.base import CRUDBase
+from app.core.utils import generate_id
+
+
+class PriceListCRUD(CRUDBase[BizPriceList]):
+ """价格清单CRUD操作"""
+
+ def get_by_region(self, db: Session, region_id: str) -> List[BizPriceList]:
+ """根据区域获取价格清单列表"""
+ return db.query(BizPriceList).filter(
+ and_(
+ BizPriceList.region_id == region_id,
+ BizPriceList.is_del == 0
+ )
+ ).order_by(BizPriceList.create_at.desc()).all()
+
+ def get_active(self, db: Session, region_id: str) -> Optional[BizPriceList]:
+ """获取区域当前生效的价格清单"""
+ return db.query(BizPriceList).filter(
+ and_(
+ BizPriceList.region_id == region_id,
+ BizPriceList.status == "active",
+ BizPriceList.is_del == 0
+ )
+ ).first()
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ region_id: str = None,
+ supplier_id: str = None,
+ status: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizPriceList], int]:
+ """多条件查询价格清单"""
+ query = db.query(BizPriceList).filter(BizPriceList.is_del == 0)
+
+ if region_id:
+ query = query.filter(BizPriceList.region_id == region_id)
+ if supplier_id:
+ query = query.filter(BizPriceList.supplier_id == supplier_id)
+ if status:
+ query = query.filter(BizPriceList.status == status)
+
+ total = query.count()
+ price_lists = query.order_by(BizPriceList.create_at.desc()).offset(skip).limit(limit).all()
+ return price_lists, total
+
+ def create_with_items(
+ self,
+ db: Session,
+ obj_in: dict,
+ items: List[dict],
+ creator: dict = None
+ ) -> BizPriceList:
+ """创建价格清单及其明细"""
+ # 计算版本号
+ version = 1
+ if obj_in.get("region_id"):
+ existing = db.query(BizPriceList).filter(
+ and_(
+ BizPriceList.region_id == obj_in["region_id"],
+ BizPriceList.is_del == 0
+ )
+ ).order_by(BizPriceList.version.desc()).first()
+ if existing:
+ version = existing.version + 1
+
+ price_list = BizPriceList(
+ id=generate_id(),
+ region_id=obj_in["region_id"],
+ supplier_id=obj_in["supplier_id"],
+ version=version,
+ status="draft",
+ effective_time=obj_in.get("effective_time"),
+ remark=obj_in.get("remark"),
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(price_list)
+ db.flush()
+
+ # 创建价格明细
+ for item in items:
+ price_item = BizPriceListItem(
+ id=generate_id(),
+ price_list_id=price_list.id,
+ product_id=item.get("product_id"),
+ unit_id=item.get("unit_id"),
+ price=Decimal(str(item.get("price", 0))),
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(price_item)
+
+ db.commit()
+ db.refresh(price_list)
+ return price_list
+
+ def update_with_items(
+ self,
+ db: Session,
+ id: str,
+ obj_in: dict,
+ items: List[dict] = None,
+ updater: dict = None
+ ) -> Optional[BizPriceList]:
+ """更新价格清单及其明细(仅草稿状态可编辑)"""
+ price_list = self.get(db, id)
+ if not price_list:
+ return None
+
+ if price_list.status != "draft":
+ return None
+
+ # 更新主表
+ if obj_in.get("effective_time") is not None:
+ price_list.effective_time = obj_in["effective_time"]
+ if obj_in.get("remark") is not None:
+ price_list.remark = obj_in["remark"]
+ if updater:
+ price_list.update_by = updater.get("user_id")
+
+ # 如果有新的价格项,则替换
+ if items is not None:
+ # 删除旧的价格项
+ db.query(BizPriceListItem).filter(
+ BizPriceListItem.price_list_id == id
+ ).delete()
+
+ # 添加新的价格项
+ for item in items:
+ price_item = BizPriceListItem(
+ id=generate_id(),
+ price_list_id=id,
+ product_id=item.get("product_id"),
+ unit_id=item.get("unit_id"),
+ price=Decimal(str(item.get("price", 0))),
+ create_by=updater.get("user_id") if updater else None,
+ create_name=updater.get("real_name") if updater else None
+ )
+ db.add(price_item)
+
+ db.commit()
+ db.refresh(price_list)
+ return price_list
+
+ def publish(
+ self,
+ db: Session,
+ id: str,
+ publisher_id: str,
+ effective_time: datetime = None,
+ effective_type: str = "immediate"
+ ) -> Optional[BizPriceList]:
+ """发布价格清单"""
+ price_list = self.get(db, id)
+ if not price_list:
+ return None
+
+ if price_list.status != "draft":
+ return None
+
+ # 如果是立即生效,设为active状态
+ if effective_type == "immediate":
+ # 先将其他生效中的价格清单设为失效
+ existing_active = db.query(BizPriceList).filter(
+ and_(
+ BizPriceList.region_id == price_list.region_id,
+ BizPriceList.status == "active",
+ BizPriceList.id != id,
+ BizPriceList.is_del == 0
+ )
+ ).all()
+ for pl in existing_active:
+ pl.status = "inactive"
+
+ price_list.status = "active"
+ price_list.effective_time = datetime.now()
+ else:
+ price_list.status = "pending"
+ if effective_time:
+ price_list.effective_time = effective_time
+
+ price_list.published_by = publisher_id
+ price_list.published_time = datetime.now()
+
+ db.commit()
+ db.refresh(price_list)
+ return price_list
+
+ def invalidate(
+ self,
+ db: Session,
+ id: str
+ ) -> Optional[BizPriceList]:
+ """使价格清单失效"""
+ price_list = self.get(db, id)
+ if not price_list:
+ return None
+
+ if price_list.status in ["active", "pending"]:
+ price_list.status = "inactive"
+ db.commit()
+ db.refresh(price_list)
+
+ return price_list
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizPriceList]:
+ """更新价格清单并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "region_id": original.region_id,
+ "supplier_id": original.supplier_id,
+ "version": original.version,
+ "status": original.status,
+ "effective_time": original.effective_time,
+ "published_by": original.published_by,
+ "published_time": original.published_time,
+ "remark": original.remark,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizPriceListHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, id: str) -> bool:
+ """删除价格清单并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return False
+
+ his_data = {
+ "id": original.id,
+ "region_id": original.region_id,
+ "supplier_id": original.supplier_id,
+ "version": original.version,
+ "status": original.status,
+ "effective_time": original.effective_time,
+ "published_by": original.published_by,
+ "published_time": original.published_time,
+ "remark": original.remark,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizPriceListHis(**his_data)
+ db.add(his)
+
+ return self.delete(db, id)
+
+
+class PriceListItemCRUD(CRUDBase[BizPriceListItem]):
+ """价格清单项CRUD操作"""
+
+ def get_by_price_list(self, db: Session, price_list_id: str) -> List[BizPriceListItem]:
+ """获取价格清单的所有明细"""
+ return db.query(BizPriceListItem).filter(
+ and_(
+ BizPriceListItem.price_list_id == price_list_id,
+ BizPriceListItem.is_del == 0
+ )
+ ).all()
+
+ def get_multi_by_price_lists(
+ self,
+ db: Session,
+ price_list_ids: List[str]
+ ) -> List[BizPriceListItem]:
+ """根据多个价格清单ID获取明细"""
+ return db.query(BizPriceListItem).filter(
+ and_(
+ BizPriceListItem.price_list_id.in_(price_list_ids),
+ BizPriceListItem.is_del == 0
+ )
+ ).all()
+
+ def get_product_price(
+ self,
+ db: Session,
+ region_id: str,
+ product_id: str
+ ) -> Optional[BizPriceListItem]:
+ """获取当前生效价格清单中指定商品的价格"""
+ # 先获取当前生效的价格清单
+ active_price_list = db.query(BizPriceList).filter(
+ and_(
+ BizPriceList.region_id == region_id,
+ BizPriceList.status == "active",
+ BizPriceList.is_del == 0
+ )
+ ).first()
+
+ if not active_price_list:
+ return None
+
+ return db.query(BizPriceListItem).filter(
+ and_(
+ BizPriceListItem.price_list_id == active_price_list.id,
+ BizPriceListItem.product_id == product_id,
+ BizPriceListItem.is_del == 0
+ )
+ ).first()
+
+ def get_product_price_history(
+ self,
+ db: Session,
+ region_id: str,
+ product_id: str,
+ skip: int = 0,
+ limit: int = 20
+ ) -> tuple[List[dict], int]:
+ """获取商品价格历史"""
+ # 获取所有生效过的价格清单及其明细
+ price_lists = db.query(BizPriceList).filter(
+ and_(
+ BizPriceList.region_id == region_id,
+ BizPriceList.status.in_(["active", "inactive"]),
+ BizPriceList.is_del == 0
+ )
+ ).order_by(BizPriceList.published_time.desc()).all()
+
+ price_list_ids = [pl.id for pl in price_lists]
+
+ items = db.query(BizPriceListItem).filter(
+ and_(
+ BizPriceListItem.price_list_id.in_(price_list_ids),
+ BizPriceListItem.product_id == product_id,
+ BizPriceListItem.is_del == 0
+ )
+ ).all()
+
+ # 关联价格清单信息
+ result = []
+ for item in items:
+ price_list = next((pl for pl in price_lists if pl.id == item.price_list_id), None)
+ if price_list:
+ result.append({
+ "price": item.price,
+ "unit_id": item.unit_id,
+ "price_list_id": item.price_list_id,
+ "version": price_list.version,
+ "published_time": price_list.published_time,
+ "status": price_list.status
+ })
+
+ total = len(result)
+ return result[skip:skip+limit], total
+
+ def get_by_product_and_price_list(
+ self,
+ db: Session,
+ product_id: str,
+ price_list_id: str
+ ) -> Optional[BizPriceListItem]:
+ """根据商品ID和价格清单ID获取价格项"""
+ return db.query(BizPriceListItem).filter(
+ and_(
+ BizPriceListItem.product_id == product_id,
+ BizPriceListItem.price_list_id == price_list_id,
+ BizPriceListItem.is_del == 0
+ )
+ ).first()
+
+
+price_list_crud = PriceListCRUD(BizPriceList)
+price_list_item_crud = PriceListItemCRUD(BizPriceListItem)
diff --git a/backend/app/crud/product.py b/backend/app/crud/product.py
new file mode 100644
index 00000000..2d25d333
--- /dev/null
+++ b/backend/app/crud/product.py
@@ -0,0 +1,123 @@
+"""商品CRUD"""
+from typing import Optional, List
+from sqlalchemy.orm import Session, defer
+from sqlalchemy import and_, func, or_
+
+from app.models.models import BasProduct, BasProductHis
+from app.crud.base import CRUDBase
+
+
+class ProductCRUD(CRUDBase[BasProduct]):
+ """商品CRUD操作"""
+
+ def get_by_name(self, db: Session, name: str) -> Optional[BasProduct]:
+ """根据名称获取商品"""
+ return db.query(BasProduct).filter(
+ and_(BasProduct.name == name, BasProduct.is_del == 0)
+ ).first()
+
+ def get_active_all(self, db: Session) -> List[BasProduct]:
+ """获取所有启用的商品"""
+ return db.query(BasProduct).filter(
+ and_(BasProduct.is_del == 0, BasProduct.is_active == True)
+ ).order_by(BasProduct.create_at.desc()).all()
+
+ def get_multi_by_category(self, db: Session, category_id: str) -> List[BasProduct]:
+ """根据分类获取商品"""
+ return db.query(BasProduct).filter(
+ and_(BasProduct.category_id == category_id, BasProduct.is_del == 0, BasProduct.is_active == True)
+ ).order_by(BasProduct.create_at.desc()).all()
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ category_id: str = None,
+ keyword: str = None,
+ is_active: bool = None,
+ skip: int = 0,
+ limit: int = 10,
+ *,
+ defer_detail_content: bool = False,
+ ) -> tuple[List[BasProduct], int]:
+ """多条件查询商品"""
+ filters = [BasProduct.is_del == 0]
+ if category_id:
+ filters.append(BasProduct.category_id == category_id)
+ if is_active is not None:
+ filters.append(BasProduct.is_active == is_active)
+ if keyword:
+ filters.append(
+ or_(
+ BasProduct.name.like(f"%{keyword}%"),
+ BasProduct.description.like(f"%{keyword}%"),
+ )
+ )
+
+ total = (
+ db.query(func.count(BasProduct.id))
+ .filter(*filters)
+ .order_by(None)
+ .scalar()
+ or 0
+ )
+
+ query = db.query(BasProduct).filter(*filters)
+ if defer_detail_content:
+ query = query.options(defer(BasProduct.detail_content))
+ products = query.order_by(BasProduct.create_at.desc()).offset(skip).limit(limit).all()
+ return products, total
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BasProduct]:
+ """更新商品并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "name": original.name,
+ "category_id": original.category_id,
+ "unit_id": original.unit_id,
+ "measure_value": getattr(original, "measure_value", None),
+ "description": original.description,
+ "image_url": original.image_url,
+ "is_active": original.is_active,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BasProductHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, id: str) -> bool:
+ """删除商品并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return False
+
+ his_data = {
+ "id": original.id,
+ "name": original.name,
+ "category_id": original.category_id,
+ "unit_id": original.unit_id,
+ "measure_value": getattr(original, "measure_value", None),
+ "description": original.description,
+ "image_url": original.image_url,
+ "is_active": original.is_active,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BasProductHis(**his_data)
+ db.add(his)
+
+ return self.delete(db, id)
+
+
+product_crud = ProductCRUD(BasProduct)
diff --git a/backend/app/crud/product_banner.py b/backend/app/crud/product_banner.py
new file mode 100644
index 00000000..862663f4
--- /dev/null
+++ b/backend/app/crud/product_banner.py
@@ -0,0 +1,73 @@
+"""CRUD - 商品轮播图"""
+from typing import List, Optional
+from sqlalchemy.orm import Session
+
+from app.crud.base import CRUDBase, HistoryCRUD
+from app.models.product_banner import BasProductBanner, BasProductBannerHis
+from app.schemas.product_banner import ProductBannerCreate, ProductBannerUpdate
+
+
+class ProductBannerCRUD(CRUDBase[BasProductBanner]):
+ """商品轮播图CRUD操作"""
+
+ def get_by_product(self, db: Session, product_id: str) -> List[BasProductBanner]:
+ """获取商品的所有轮播图"""
+ return db.query(self.model).filter(
+ self.model.product_id == product_id,
+ self.model.is_del == 0
+ ).order_by(self.model.sort_order.asc()).all()
+
+ def create_batch(
+ self,
+ db: Session,
+ product_id: str,
+ items: List[dict],
+ creator: dict = None
+ ) -> List[BasProductBanner]:
+ """批量创建轮播图"""
+ banners = []
+ for idx, item in enumerate(items):
+ item_data = {
+ "product_id": product_id,
+ "media_type": item.get("media_type", "image"),
+ "file_url": item.get("file_url", ""),
+ "thumbnail_url": item.get("thumbnail_url"),
+ "sort_order": item.get("sort_order", idx),
+ }
+ banner = self.create(db, item_data, creator)
+ banners.append(banner)
+ return banners
+
+ def update_sort(
+ self,
+ db: Session,
+ items: List[dict]
+ ) -> bool:
+ """批量更新排序"""
+ for item in items:
+ banner_id = item.get("id")
+ sort_order = item.get("sort_order")
+ if banner_id and sort_order is not None:
+ db.query(self.model).filter(self.model.id == banner_id).update({
+ "sort_order": sort_order
+ })
+ db.commit()
+ return True
+
+ def delete_by_product(self, db: Session, product_id: str) -> int:
+ """删除商品的所有轮播图"""
+ count = db.query(self.model).filter(
+ self.model.product_id == product_id,
+ self.model.is_del == 0
+ ).update({"is_del": 1})
+ db.commit()
+ return count
+
+
+class ProductBannerHisCRUD(HistoryCRUD[BasProductBannerHis]):
+ """商品轮播图历史CRUD操作"""
+ pass
+
+
+product_banner = ProductBannerCRUD(BasProductBanner)
+product_banner_his = ProductBannerHisCRUD(BasProductBannerHis)
diff --git a/backend/app/crud/product_detail_media.py b/backend/app/crud/product_detail_media.py
new file mode 100644
index 00000000..1502517c
--- /dev/null
+++ b/backend/app/crud/product_detail_media.py
@@ -0,0 +1,75 @@
+"""CRUD - 商品详情媒体"""
+from typing import List
+from sqlalchemy.orm import Session
+
+from app.crud.base import CRUDBase, HistoryCRUD
+from app.models.product_detail_media import BasProductDetailMedia, BasProductDetailMediaHis
+from app.schemas.product_detail_media import ProductDetailMediaCreate, ProductDetailMediaUpdate
+
+
+class ProductDetailMediaCRUD(CRUDBase[BasProductDetailMedia]):
+ """商品详情媒体CRUD操作"""
+
+ def get_by_product(self, db: Session, product_id: str) -> List[BasProductDetailMedia]:
+ """获取商品的所有详情媒体"""
+ return db.query(self.model).filter(
+ self.model.product_id == product_id,
+ self.model.is_del == 0
+ ).order_by(self.model.sort_order.asc()).all()
+
+ def create_batch(
+ self,
+ db: Session,
+ product_id: str,
+ items: List[dict],
+ creator: dict = None
+ ) -> List[BasProductDetailMedia]:
+ """批量创建详情媒体"""
+ media_list = []
+ for idx, item in enumerate(items):
+ item_data = {
+ "product_id": product_id,
+ "content_type": item.get("content_type", "image"),
+ "content_text": item.get("content_text"),
+ "file_url": item.get("file_url"),
+ "thumbnail_url": item.get("thumbnail_url"),
+ "description": item.get("description"),
+ "sort_order": item.get("sort_order", idx),
+ }
+ media = self.create(db, item_data, creator)
+ media_list.append(media)
+ return media_list
+
+ def update_sort(
+ self,
+ db: Session,
+ items: List[dict]
+ ) -> bool:
+ """批量更新排序"""
+ for item in items:
+ media_id = item.get("id")
+ sort_order = item.get("sort_order")
+ if media_id and sort_order is not None:
+ db.query(self.model).filter(self.model.id == media_id).update({
+ "sort_order": sort_order
+ })
+ db.commit()
+ return True
+
+ def delete_by_product(self, db: Session, product_id: str) -> int:
+ """删除商品的所有详情媒体"""
+ count = db.query(self.model).filter(
+ self.model.product_id == product_id,
+ self.model.is_del == 0
+ ).update({"is_del": 1})
+ db.commit()
+ return count
+
+
+class ProductDetailMediaHisCRUD(HistoryCRUD[BasProductDetailMediaHis]):
+ """商品详情媒体历史CRUD操作"""
+ pass
+
+
+product_detail_media = ProductDetailMediaCRUD(BasProductDetailMedia)
+product_detail_media_his = ProductDetailMediaHisCRUD(BasProductDetailMediaHis)
diff --git a/backend/app/crud/product_parameter.py b/backend/app/crud/product_parameter.py
new file mode 100644
index 00000000..d0eaead5
--- /dev/null
+++ b/backend/app/crud/product_parameter.py
@@ -0,0 +1,83 @@
+"""CRUD - 商品参数模板"""
+from typing import List, Optional
+from sqlalchemy.orm import Session
+from sqlalchemy import or_
+
+from app.crud.base import CRUDBase, HistoryCRUD
+from app.models.product_parameter import BasProductParameter, BasProductParameterHis
+from app.schemas.product_parameter import ProductParameterCreate, ProductParameterUpdate
+
+
+class ProductParameterCRUD(CRUDBase[BasProductParameter]):
+ """商品参数模板CRUD操作"""
+
+ def get_multi_paged(
+ self,
+ db: Session,
+ skip: int = 0,
+ limit: int = 10,
+ is_active: Optional[int] = None,
+ keyword: Optional[str] = None,
+ order_by: str = "sort_order",
+ order_type: str = "asc"
+ ) -> tuple:
+ """分页获取参数列表"""
+ query = db.query(self.model).filter(self.model.is_del == 0)
+
+ if is_active is not None:
+ query = query.filter(self.model.is_active == is_active)
+
+ if keyword:
+ query = query.filter(
+ or_(
+ self.model.name.like(f"%{keyword}%"),
+ self.model.code.like(f"%{keyword}%")
+ )
+ )
+
+ total = query.count()
+
+ order_column = getattr(self.model, order_by, None)
+ if order_column:
+ if order_type == "asc":
+ query = query.order_by(order_column.asc())
+ else:
+ query = query.order_by(order_column.desc())
+ else:
+ query = query.order_by(self.model.sort_order.asc())
+
+ items = query.offset(skip).limit(limit).all()
+ return items, total
+
+ def get_active_all(self, db: Session) -> List[BasProductParameter]:
+ """获取所有启用的参数"""
+ return db.query(self.model).filter(
+ self.model.is_del == 0,
+ self.model.is_active == 1
+ ).order_by(self.model.sort_order.asc()).all()
+
+ def get_by_code(self, db: Session, code: str) -> Optional[BasProductParameter]:
+ """根据编码获取参数"""
+ return db.query(self.model).filter(
+ self.model.code == code,
+ self.model.is_del == 0
+ ).first()
+
+ def code_exists(self, db: Session, code: str, exclude_id: str = None) -> bool:
+ """检查编码是否已存在"""
+ query = db.query(self.model).filter(
+ self.model.code == code,
+ self.model.is_del == 0
+ )
+ if exclude_id:
+ query = query.filter(self.model.id != exclude_id)
+ return query.first() is not None
+
+
+class ProductParameterHisCRUD(HistoryCRUD[BasProductParameterHis]):
+ """商品参数模板历史CRUD操作"""
+ pass
+
+
+product_parameter = ProductParameterCRUD(BasProductParameter)
+product_parameter_his = ProductParameterHisCRUD(BasProductParameterHis)
diff --git a/backend/app/crud/product_parameter_value.py b/backend/app/crud/product_parameter_value.py
new file mode 100644
index 00000000..1cd73433
--- /dev/null
+++ b/backend/app/crud/product_parameter_value.py
@@ -0,0 +1,117 @@
+"""CRUD - 商品参数值"""
+from typing import List
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from app.crud.base import CRUDBase, HistoryCRUD
+from app.models.product_parameter_value import BasProductParameterValue, BasProductParameterValueHis
+from app.schemas.product_parameter_value import ProductParameterValueCreate
+
+
+class ProductParameterValueCRUD(CRUDBase[BasProductParameterValue]):
+ """商品参数值CRUD操作"""
+
+ def get_by_product(self, db: Session, product_id: str) -> List[BasProductParameterValue]:
+ """获取商品的所有参数值"""
+ return db.query(self.model).filter(
+ self.model.product_id == product_id,
+ self.model.is_del == 0
+ ).order_by(self.model.sort_order.asc()).all()
+
+ def get_by_product_with_detail(self, db: Session, product_id: str) -> List[dict]:
+ """获取商品的所有参数值(带参数详情)"""
+ from app.models.product_parameter import BasProductParameter
+
+ results = db.query(
+ BasProductParameterValue,
+ BasProductParameter
+ ).join(
+ BasProductParameter,
+ and_(
+ BasProductParameterValue.parameter_id == BasProductParameter.id,
+ BasProductParameter.is_del == 0
+ )
+ ).filter(
+ BasProductParameterValue.product_id == product_id,
+ BasProductParameterValue.is_del == 0
+ ).order_by(
+ BasProductParameterValue.sort_order.asc()
+ ).all()
+
+ return [
+ {
+ "id": ppv.id,
+ "product_id": ppv.product_id,
+ "parameter_id": ppv.parameter_id,
+ "parameter_name": pp.name,
+ "parameter_code": pp.code,
+ "param_type": pp.param_type,
+ "options": pp.options,
+ "placeholder": pp.placeholder,
+ "param_value": ppv.param_value,
+ "sort_order": ppv.sort_order,
+ }
+ for ppv, pp in results
+ ]
+
+ def batch_save(
+ self,
+ db: Session,
+ product_id: str,
+ values: List[dict],
+ creator: dict = None
+ ) -> List[BasProductParameterValue]:
+ """批量保存商品参数值"""
+ from app.models.product_parameter import BasProductParameter
+
+ # 先删除原有的参数值
+ db.query(self.model).filter(
+ self.model.product_id == product_id,
+ self.model.is_del == 0
+ ).update({"is_del": 1})
+
+ # 创建新的参数值
+ saved_values = []
+ for idx, value_item in enumerate(values):
+ parameter_id = value_item.get("parameter_id")
+ param_value = value_item.get("param_value")
+
+ # 获取参数信息
+ param = db.query(BasProductParameter).filter(
+ BasProductParameter.id == parameter_id,
+ BasProductParameter.is_del == 0
+ ).first()
+
+ if param:
+ item_data = {
+ "product_id": product_id,
+ "parameter_id": parameter_id,
+ "parameter_name": param.name,
+ "parameter_code": param.code,
+ "param_type": param.param_type,
+ "param_value": param_value,
+ "sort_order": idx,
+ }
+ pv = self.create(db, item_data, creator)
+ saved_values.append(pv)
+
+ db.commit()
+ return saved_values
+
+ def delete_by_product(self, db: Session, product_id: str) -> int:
+ """删除商品的所有参数值"""
+ count = db.query(self.model).filter(
+ self.model.product_id == product_id,
+ self.model.is_del == 0
+ ).update({"is_del": 1})
+ db.commit()
+ return count
+
+
+class ProductParameterValueHisCRUD(HistoryCRUD[BasProductParameterValueHis]):
+ """商品参数值历史CRUD操作"""
+ pass
+
+
+product_parameter_value = ProductParameterValueCRUD(BasProductParameterValue)
+product_parameter_value_his = ProductParameterValueHisCRUD(BasProductParameterValueHis)
diff --git a/backend/app/crud/product_review.py b/backend/app/crud/product_review.py
new file mode 100644
index 00000000..389f8fcb
--- /dev/null
+++ b/backend/app/crud/product_review.py
@@ -0,0 +1,242 @@
+"""CRUD - 售后评价"""
+from typing import List, Optional
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, func
+
+from app.crud.base import CRUDBase, HistoryCRUD
+from app.models.product_review import BizProductReview, BizProductReviewHis
+from app.schemas.product_review import ProductReviewCreate, ProductReviewReplyRequest
+
+
+class ProductReviewCRUD(CRUDBase[BizProductReview]):
+ """售后评价CRUD操作"""
+
+ def get_by_product(
+ self,
+ db: Session,
+ product_id: str,
+ skip: int = 0,
+ limit: int = 10,
+ status: str = "approved",
+ has_media: int = None
+ ) -> tuple:
+ """获取商品的评价列表"""
+ query = db.query(self.model).filter(
+ self.model.product_id == product_id,
+ self.model.is_del == 0
+ )
+
+ if status:
+ query = query.filter(self.model.status == status)
+
+ if has_media == 1:
+ from app.models.product_review_media import BizProductReviewMedia
+ review_ids_with_media = db.query(BizProductReviewMedia.review_id).filter(
+ BizProductReviewMedia.is_del == 0
+ ).distinct().subquery()
+ query = query.filter(self.model.id.in_(review_ids_with_media))
+
+ total = query.count()
+
+ items = query.order_by(
+ self.model.is_top.desc(),
+ self.model.create_at.desc()
+ ).offset(skip).limit(limit).all()
+
+ return items, total
+
+ def get_admin_list(
+ self,
+ db: Session,
+ skip: int = 0,
+ limit: int = 10,
+ product_id: str = None,
+ product_name: str = None,
+ user_name: str = None,
+ status: str = None,
+ rating: int = None,
+ start_date: str = None,
+ end_date: str = None,
+ ) -> tuple:
+ """管理员获取评价列表(支持多条件筛选)"""
+ query = db.query(self.model).filter(self.model.is_del == 0)
+
+ if product_id:
+ query = query.filter(self.model.product_id == product_id)
+
+ if user_name:
+ query = query.filter(self.model.user_name.like(f"%{user_name}%"))
+
+ if status:
+ query = query.filter(self.model.status == status)
+
+ if rating:
+ query = query.filter(self.model.rating == rating)
+
+ if start_date:
+ query = query.filter(self.model.create_at >= start_date)
+
+ if end_date:
+ query = query.filter(self.model.create_at <= end_date)
+
+ total = query.count()
+
+ items = query.order_by(
+ self.model.is_top.desc(),
+ self.model.create_at.desc()
+ ).offset(skip).limit(limit).all()
+
+ return items, total
+
+ def get_statistics(self, db: Session, product_id: str = None) -> dict:
+ """获取评价统计"""
+ query = db.query(self.model).filter(
+ self.model.is_del == 0,
+ self.model.status == "approved"
+ )
+
+ if product_id:
+ query = query.filter(self.model.product_id == product_id)
+
+ total_count = query.count()
+
+ if total_count > 0:
+ avg_rating = db.query(func.avg(self.model.rating)).filter(
+ self.model.is_del == 0,
+ self.model.status == "approved",
+ self.model.product_id == product_id if product_id else True
+ ).scalar() or 0
+ else:
+ avg_rating = 0
+
+ rating_dist = {}
+ for r in range(1, 6):
+ count = db.query(self.model).filter(
+ self.model.is_del == 0,
+ self.model.status == "approved",
+ self.model.rating == r,
+ self.model.product_id == product_id if product_id else True
+ ).count()
+ rating_dist[str(r)] = count
+
+ pending_count = db.query(self.model).filter(
+ self.model.is_del == 0,
+ self.model.status == "pending",
+ self.model.product_id == product_id if product_id else True
+ ).count()
+
+ rejected_count = db.query(self.model).filter(
+ self.model.is_del == 0,
+ self.model.status == "rejected",
+ self.model.product_id == product_id if product_id else True
+ ).count()
+
+ return {
+ "total_count": total_count,
+ "pending_count": pending_count,
+ "approved_count": total_count,
+ "rejected_count": rejected_count,
+ "avg_rating": round(float(avg_rating), 1),
+ "rating_distribution": rating_dist,
+ }
+
+ def reply(
+ self,
+ db: Session,
+ review_id: str,
+ reply_content: str,
+ updater: dict = None
+ ) -> Optional[BizProductReview]:
+ """回复评价"""
+ review = self.get(db, review_id)
+ if review:
+ review.reply_content = reply_content
+ review.reply_time = func.now()
+ if updater:
+ review.reply_by = updater.get("user_id")
+ review.reply_by_name = updater.get("real_name")
+ db.commit()
+ db.refresh(review)
+ return review
+
+ def reject(
+ self,
+ db: Session,
+ review_id: str,
+ reject_reason: str,
+ updater: dict = None
+ ) -> Optional[BizProductReview]:
+ """拒绝评价"""
+ review = self.get(db, review_id)
+ if review:
+ review.status = "rejected"
+ review.reject_reason = reject_reason
+ db.commit()
+ db.refresh(review)
+ return review
+
+ def approve(
+ self,
+ db: Session,
+ review_id: str
+ ) -> Optional[BizProductReview]:
+ """审核通过评价"""
+ review = self.get(db, review_id)
+ if review:
+ review.status = "approved"
+ db.commit()
+ db.refresh(review)
+ return review
+
+ def toggle_top(
+ self,
+ db: Session,
+ review_id: str,
+ is_top: int
+ ) -> Optional[BizProductReview]:
+ """切换置顶状态"""
+ review = self.get(db, review_id)
+ if review:
+ review.is_top = is_top
+ db.commit()
+ db.refresh(review)
+ return review
+
+ def like(
+ self,
+ db: Session,
+ review_id: str
+ ) -> Optional[BizProductReview]:
+ """点赞评价"""
+ review = self.get(db, review_id)
+ if review:
+ review.like_count = (review.like_count or 0) + 1
+ db.commit()
+ db.refresh(review)
+ return review
+
+ def user_can_review(
+ self,
+ db: Session,
+ user_id: str,
+ product_id: str,
+ order_id: str = None
+ ) -> bool:
+ """检查用户是否已评价过该商品"""
+ query = db.query(self.model).filter(
+ self.model.user_id == user_id,
+ self.model.product_id == product_id,
+ self.model.is_del == 0
+ )
+ if order_id:
+ query = query.filter(self.model.order_id == order_id)
+ return query.first() is not None
+
+
+class ProductReviewHisCRUD(HistoryCRUD[BizProductReviewHis]):
+ """售后评价历史CRUD操作"""
+ pass
+
+
+product_review = ProductReviewCRUD(BizProductReview)
+product_review_his = ProductReviewHisCRUD(BizProductReviewHis)
diff --git a/backend/app/crud/product_review_media.py b/backend/app/crud/product_review_media.py
new file mode 100644
index 00000000..a4a8af5d
--- /dev/null
+++ b/backend/app/crud/product_review_media.py
@@ -0,0 +1,63 @@
+"""CRUD - 评价媒体"""
+from typing import List
+from sqlalchemy.orm import Session
+
+from app.crud.base import CRUDBase, HistoryCRUD
+from app.models.product_review_media import BizProductReviewMedia, BizProductReviewMediaHis
+
+
+class ProductReviewMediaCRUD(CRUDBase[BizProductReviewMedia]):
+ """评价媒体CRUD操作"""
+
+ def get_by_review(self, db: Session, review_id: str) -> List[BizProductReviewMedia]:
+ """获取评价的所有媒体"""
+ return db.query(self.model).filter(
+ self.model.review_id == review_id,
+ self.model.is_del == 0
+ ).order_by(self.model.sort_order.asc()).all()
+
+ def create_batch(
+ self,
+ db: Session,
+ review_id: str,
+ items: List[dict],
+ creator: dict = None
+ ) -> List[BizProductReviewMedia]:
+ """批量创建评价媒体"""
+ media_list = []
+ for idx, item in enumerate(items):
+ item_data = {
+ "review_id": review_id,
+ "media_type": item.get("media_type", "image"),
+ "file_url": item.get("file_url", ""),
+ "thumbnail_url": item.get("thumbnail_url"),
+ "sort_order": item.get("sort_order", idx),
+ }
+ media = self.create(db, item_data, creator)
+ media_list.append(media)
+ return media_list
+
+ def delete_by_review(self, db: Session, review_id: str) -> int:
+ """删除评价的所有媒体"""
+ count = db.query(self.model).filter(
+ self.model.review_id == review_id,
+ self.model.is_del == 0
+ ).update({"is_del": 1})
+ db.commit()
+ return count
+
+ def get_media_count(self, db: Session, review_id: str) -> int:
+ """获取评价的媒体数量"""
+ return db.query(self.model).filter(
+ self.model.review_id == review_id,
+ self.model.is_del == 0
+ ).count()
+
+
+class ProductReviewMediaHisCRUD(HistoryCRUD[BizProductReviewMediaHis]):
+ """评价媒体历史CRUD操作"""
+ pass
+
+
+product_review_media = ProductReviewMediaCRUD(BizProductReviewMedia)
+product_review_media_his = ProductReviewMediaHisCRUD(BizProductReviewMediaHis)
diff --git a/backend/app/crud/quotation.py b/backend/app/crud/quotation.py
new file mode 100644
index 00000000..8d0bb7ca
--- /dev/null
+++ b/backend/app/crud/quotation.py
@@ -0,0 +1,201 @@
+"""报价单CRUD"""
+from decimal import Decimal
+from typing import Optional, List
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, func
+
+from app.models.models import BizQuotation, BizQuotationItem, BizQuotationHis, BizQuotationItemHis
+from app.crud.base import CRUDBase
+from app.core.utils import generate_id
+
+
+class QuotationCRUD(CRUDBase[BizQuotation]):
+ """报价单CRUD操作"""
+
+ def get_by_inquiry(self, db: Session, inquiry_id: str) -> List[BizQuotation]:
+ """根据询价单获取报价列表"""
+ return db.query(BizQuotation).filter(
+ and_(
+ BizQuotation.inquiry_id == inquiry_id,
+ BizQuotation.is_del == 0
+ )
+ ).all()
+
+ def get_by_supplier(self, db: Session, supplier_id: str) -> List[BizQuotation]:
+ """根据供应商获取报价列表"""
+ return db.query(BizQuotation).filter(
+ and_(
+ BizQuotation.supplier_id == supplier_id,
+ BizQuotation.is_del == 0
+ )
+ ).order_by(BizQuotation.create_at.desc()).all()
+
+ def get_by_inquiry_and_supplier(
+ self,
+ db: Session,
+ inquiry_id: str,
+ supplier_id: str
+ ) -> Optional[BizQuotation]:
+ """根据询价单和供应商获取报价"""
+ return db.query(BizQuotation).filter(
+ and_(
+ BizQuotation.inquiry_id == inquiry_id,
+ BizQuotation.supplier_id == supplier_id,
+ BizQuotation.is_del == 0
+ )
+ ).first()
+
+ def get_latest_by_inquiry_and_supplier(
+ self,
+ db: Session,
+ inquiry_id: str,
+ supplier_id: str
+ ) -> Optional[BizQuotation]:
+ """获取某供应商在某询价单下最新一次报价"""
+ return db.query(BizQuotation).filter(
+ and_(
+ BizQuotation.inquiry_id == inquiry_id,
+ BizQuotation.supplier_id == supplier_id,
+ BizQuotation.is_del == 0
+ )
+ ).order_by(BizQuotation.create_at.desc()).first()
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ inquiry_id: str = None,
+ supplier_id: str = None,
+ region_id: str = None,
+ status: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[BizQuotation], int]:
+ """多条件查询报价单"""
+ query = db.query(BizQuotation).filter(BizQuotation.is_del == 0)
+
+ if inquiry_id:
+ query = query.filter(BizQuotation.inquiry_id == inquiry_id)
+ if supplier_id:
+ query = query.filter(BizQuotation.supplier_id == supplier_id)
+ if region_id:
+ query = query.filter(BizQuotation.region_id == region_id)
+ if status:
+ query = query.filter(BizQuotation.status == status)
+
+ total = query.count()
+ quotations = query.order_by(BizQuotation.create_at.desc()).offset(skip).limit(limit).all()
+ return quotations, total
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizQuotation]:
+ """更新报价单并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "inquiry_id": original.inquiry_id,
+ "supplier_id": original.supplier_id,
+ "region_id": original.region_id,
+ "status": original.status,
+ "total_amount": original.total_amount,
+ "submit_time": original.submit_time,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizQuotationHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, id: str) -> bool:
+ """删除报价单并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return False
+
+ his_data = {
+ "id": original.id,
+ "inquiry_id": original.inquiry_id,
+ "supplier_id": original.supplier_id,
+ "region_id": original.region_id,
+ "status": original.status,
+ "total_amount": original.total_amount,
+ "submit_time": original.submit_time,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizQuotationHis(**his_data)
+ db.add(his)
+
+ return self.delete(db, id)
+
+
+class QuotationItemCRUD(CRUDBase[BizQuotationItem]):
+ """报价单项CRUD操作"""
+
+ def get_by_quotation(self, db: Session, quotation_id: str) -> List[BizQuotationItem]:
+ """获取报价单的所有报价项"""
+ return db.query(BizQuotationItem).filter(
+ and_(
+ BizQuotationItem.quotation_id == quotation_id,
+ BizQuotationItem.is_del == 0
+ )
+ ).all()
+
+ def get_multi_by_quotations(
+ self,
+ db: Session,
+ quotation_ids: List[str]
+ ) -> List[BizQuotationItem]:
+ """根据多个报价单ID获取报价项"""
+ return db.query(BizQuotationItem).filter(
+ and_(
+ BizQuotationItem.quotation_id.in_(quotation_ids),
+ BizQuotationItem.is_del == 0
+ )
+ ).all()
+
+ def create_items(
+ self,
+ db: Session,
+ quotation_id: str,
+ items: List[dict],
+ creator: dict = None
+ ) -> List[BizQuotationItem]:
+ """批量创建报价项"""
+ result = []
+ for item in items:
+ quotation_item = BizQuotationItem(
+ id=generate_id(),
+ quotation_id=quotation_id,
+ product_id=item.get("product_id"),
+ unit_id=item.get("unit_id"),
+ price=Decimal(str(item.get("price", 0))),
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(quotation_item)
+ result.append(quotation_item)
+ db.commit()
+ for item in result:
+ db.refresh(item)
+ return result
+
+ def delete_by_quotation(self, db: Session, quotation_id: str) -> bool:
+ """删除报价单的所有报价项"""
+ items = self.get_by_quotation(db, quotation_id)
+ for item in items:
+ item.is_del = 1
+ db.commit()
+ return True
+
+
+quotation_crud = QuotationCRUD(BizQuotation)
+quotation_item_crud = QuotationItemCRUD(BizQuotationItem)
diff --git a/backend/app/crud/region.py b/backend/app/crud/region.py
new file mode 100644
index 00000000..ee5f785d
--- /dev/null
+++ b/backend/app/crud/region.py
@@ -0,0 +1,81 @@
+"""区域管理CRUD"""
+from typing import Optional, List
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from app.models.models import SysRegion, SysRegionHis
+from app.crud.base import CRUDBase
+
+
+class RegionCRUD(CRUDBase[SysRegion]):
+ """区域CRUD操作"""
+
+ def get_by_code(self, db: Session, code: str) -> Optional[SysRegion]:
+ """根据编码获取区域(含已删除,匹配数据库唯一索引约束)"""
+ return db.query(SysRegion).filter(SysRegion.code == code).first()
+
+ def get_active_all(self, db: Session) -> List[SysRegion]:
+ """获取所有启用的区域"""
+ return db.query(SysRegion).filter(
+ and_(SysRegion.is_del == 0, SysRegion.is_active == True)
+ ).order_by(SysRegion.sort_order.asc()).all()
+
+ def create_with_history(self, db: Session, obj_in: dict, creator: dict = None) -> SysRegion:
+ """创建区域并记录历史"""
+ obj = self.create(db, obj_in, creator)
+ return obj
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[SysRegion]:
+ """更新区域并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "name": original.name,
+ "code": original.code,
+ "description": original.description,
+ "sort_order": original.sort_order,
+ "expected_delivery_minutes": original.expected_delivery_minutes,
+ "delivery_warn_advance_minutes": original.delivery_warn_advance_minutes,
+ "is_active": original.is_active,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = SysRegionHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, id: str) -> bool:
+ """删除区域并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return False
+
+ his_data = {
+ "id": original.id,
+ "name": original.name,
+ "code": original.code,
+ "description": original.description,
+ "sort_order": original.sort_order,
+ "expected_delivery_minutes": original.expected_delivery_minutes,
+ "delivery_warn_advance_minutes": original.delivery_warn_advance_minutes,
+ "is_active": original.is_active,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = SysRegionHis(**his_data)
+ db.add(his)
+
+ return self.delete(db, id)
+
+
+region_crud = RegionCRUD(SysRegion)
diff --git a/backend/app/crud/settlement.py b/backend/app/crud/settlement.py
new file mode 100644
index 00000000..18669c7b
--- /dev/null
+++ b/backend/app/crud/settlement.py
@@ -0,0 +1,604 @@
+"""结算与退款CRUD"""
+from typing import Optional, List, Tuple
+from datetime import datetime
+from decimal import Decimal
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, func
+
+from app.models.models import BizSettlement, BizSettlementHis, BizRefund, BizRefundHis, BizOrder
+from app.crud.base import CRUDBase
+from app.crud.order import order_crud
+from app.core.utils import generate_id
+from app.core.order_status_compat import derive_legacy_status_from_three
+
+
+class SettlementCRUD(CRUDBase[BizSettlement]):
+ """结算记录CRUD操作"""
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ region_id: str = None,
+ client_id: str = None,
+ status: str = None,
+ start_date: str = None,
+ end_date: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> Tuple[List[BizSettlement], int]:
+ """多条件查询结算记录"""
+ query = db.query(BizSettlement).filter(BizSettlement.is_del == 0)
+
+ if region_id:
+ query = query.filter(BizSettlement.region_id == region_id)
+ if client_id:
+ query = query.filter(BizSettlement.client_id == client_id)
+ if status:
+ query = query.filter(BizSettlement.status == status)
+ if start_date:
+ query = query.filter(BizSettlement.create_at >= start_date)
+ if end_date:
+ query = query.filter(BizSettlement.create_at <= end_date)
+
+ total = query.count()
+ settlements = query.order_by(BizSettlement.create_at.desc()).offset(skip).limit(limit).all()
+ return settlements, total
+
+ def get_by_order(self, db: Session, order_id: str) -> List[BizSettlement]:
+ """根据订单获取结算记录"""
+ return db.query(BizSettlement).filter(
+ and_(
+ BizSettlement.order_id == order_id,
+ BizSettlement.is_del == 0
+ )
+ ).all()
+
+ def create_settlement(
+ self,
+ db: Session,
+ order_id: str,
+ client_id: str,
+ region_id: str,
+ amount: Decimal,
+ settlement_method: str,
+ settlement_time: datetime = None,
+ attachment_url: str = None,
+ note: str = None,
+ creator: dict = None
+ ) -> BizSettlement:
+ """创建结算记录"""
+ settlement = BizSettlement(
+ id=generate_id(),
+ order_id=order_id,
+ client_id=client_id,
+ region_id=region_id,
+ amount=amount,
+ settlement_method=settlement_method,
+ settlement_time=settlement_time or datetime.now(),
+ settlement_file_url=attachment_url,
+ settlement_note=note,
+ status="confirmed",
+ reviewed_by=creator.get("user_id") if creator else None,
+ reviewed_time=datetime.now(),
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(settlement)
+ db.flush()
+
+ # 更新订单的已付金额
+ order = db.query(BizOrder).filter(BizOrder.id == order_id).first()
+ if order:
+ order.paid_amount = (order.paid_amount or Decimal("0")) + amount
+ if order.paid_amount >= order.receivable_amount:
+ order.paid_amount = order.receivable_amount
+ order.update_at = datetime.now()
+ order_crud.sync_pay_fields_from_amounts(order)
+
+ db.commit()
+ db.refresh(settlement)
+ return settlement
+
+ def batch_create_settlements(
+ self,
+ db: Session,
+ order_ids: List[str],
+ settlement_method: str,
+ settlement_time: datetime = None,
+ attachment_url: str = None,
+ note: str = None,
+ creator: dict = None
+ ) -> List[BizSettlement]:
+ """批量创建结算记录"""
+ settlements = []
+ for order_id in order_ids:
+ order = db.query(BizOrder).filter(
+ and_(
+ BizOrder.id == order_id,
+ BizOrder.is_del == 0
+ )
+ ).first()
+
+ if order and order.status not in ["cancelled", "completed"]:
+ # 计算未付金额
+ unpaid_amount = (order.receivable_amount or Decimal("0")) - (order.paid_amount or Decimal("0"))
+
+ settlement = BizSettlement(
+ id=generate_id(),
+ order_id=order_id,
+ client_id=order.client_id,
+ region_id=order.region_id,
+ amount=unpaid_amount,
+ settlement_method=settlement_method,
+ settlement_time=settlement_time or datetime.now(),
+ settlement_file_url=attachment_url,
+ settlement_note=note,
+ status="confirmed",
+ reviewed_by=creator.get("user_id") if creator else None,
+ reviewed_time=datetime.now(),
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(settlement)
+ settlements.append(settlement)
+
+ # 更新订单的已付金额
+ order.paid_amount = order.receivable_amount
+ order.update_at = datetime.now()
+ order_crud.sync_pay_fields_from_amounts(order)
+
+ db.commit()
+ return settlements
+
+ def confirm_settlement(
+ self,
+ db: Session,
+ settlement_id: str,
+ reviewer_id: str,
+ note: str = None
+ ) -> Optional[BizSettlement]:
+ """确认结算"""
+ settlement = self.get(db, settlement_id)
+ if not settlement:
+ return None
+
+ if settlement.status != "pending":
+ return None
+
+ settlement.status = "confirmed"
+ settlement.reviewed_by = reviewer_id
+ settlement.reviewed_time = datetime.now()
+ if note:
+ settlement.settlement_note = note
+ settlement.update_at = datetime.now()
+
+ # 更新订单的已付金额
+ order = db.query(BizOrder).filter(BizOrder.id == settlement.order_id).first()
+ if order:
+ order.paid_amount = (order.paid_amount or Decimal("0")) + settlement.amount
+ if order.paid_amount >= order.receivable_amount:
+ order.paid_amount = order.receivable_amount
+ order.update_at = datetime.now()
+ order_crud.sync_pay_fields_from_amounts(order)
+
+ db.commit()
+ db.refresh(settlement)
+ return settlement
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizSettlement]:
+ """更新结算记录并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "order_id": original.order_id,
+ "client_id": original.client_id,
+ "region_id": original.region_id,
+ "amount": original.amount,
+ "settlement_method": original.settlement_method,
+ "settlement_time": original.settlement_time,
+ "settlement_file_url": original.settlement_file_url,
+ "settlement_note": original.settlement_note,
+ "status": original.status,
+ "reviewed_by": original.reviewed_by,
+ "reviewed_time": original.reviewed_time,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizSettlementHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def get_client_summary(
+ self,
+ db: Session,
+ region_id: str = None,
+ client_id: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> Tuple[List[dict], int]:
+ """获取客户待结算汇总"""
+ query = db.query(
+ BizOrder.client_id,
+ func.count(BizOrder.id).label("order_count"),
+ func.sum(BizOrder.receivable_amount - BizOrder.paid_amount).label("total_unpaid")
+ ).filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.receivable_amount > BizOrder.paid_amount,
+ BizOrder.status.notin_(["cancelled"])
+ )
+ )
+
+ if region_id:
+ query = query.filter(BizOrder.region_id == region_id)
+ if client_id:
+ query = query.filter(BizOrder.client_id == client_id)
+
+ query = query.group_by(BizOrder.client_id)
+
+ total = query.count()
+ results = query.offset(skip).limit(limit).all()
+
+ return [{"client_id": r.client_id, "order_count": r.order_count, "total_unpaid": r.total_unpaid or Decimal("0")} for r in results], total
+
+ def get_statistics(
+ self,
+ db: Session,
+ region_id: str = None,
+ start_date: str = None,
+ end_date: str = None
+ ) -> dict:
+ """获取结算统计数据"""
+ # 待结算金额(使用单条SQL查询,避免N+1问题)
+ pending_query = db.query(
+ func.count(BizOrder.id).label("order_count"),
+ func.coalesce(func.sum(BizOrder.receivable_amount - BizOrder.paid_amount), 0).label("pending_amount")
+ ).filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.receivable_amount > BizOrder.paid_amount,
+ BizOrder.status.notin_(["cancelled", "completed"])
+ )
+ )
+ if region_id:
+ pending_query = pending_query.filter(BizOrder.region_id == region_id)
+ pending_result = pending_query.first()
+
+ # 已结算金额
+ settled_query = db.query(
+ func.count(BizSettlement.id).label("settlement_count"),
+ func.coalesce(func.sum(BizSettlement.amount), 0).label("settled_amount")
+ ).filter(
+ and_(
+ BizSettlement.is_del == 0,
+ BizSettlement.status == "confirmed"
+ )
+ )
+ if region_id:
+ settled_query = settled_query.filter(BizSettlement.region_id == region_id)
+ if start_date:
+ settled_query = settled_query.filter(BizSettlement.reviewed_time >= start_date)
+ if end_date:
+ settled_query = settled_query.filter(BizSettlement.reviewed_time <= end_date)
+ settled_result = settled_query.first()
+
+ # 待处理退款数
+ refund_pending_count = db.query(func.count(BizRefund.id)).filter(
+ and_(
+ BizRefund.is_del == 0,
+ BizRefund.status.in_(["pending", "processing"])
+ )
+ )
+ if region_id:
+ refund_pending_count = refund_pending_count.join(
+ BizOrder, BizOrder.id == BizRefund.order_id
+ ).filter(BizOrder.region_id == region_id)
+ refund_pending_count = refund_pending_count.scalar() or 0
+
+ return {
+ "pendingOrderCount": pending_result.order_count or 0,
+ "pendingAmount": float(pending_result.pending_amount or 0),
+ "settledCount": settled_result.settlement_count or 0,
+ "settledAmount": float(settled_result.settled_amount or 0),
+ "refundPendingCount": refund_pending_count
+ }
+
+
+class RefundCRUD(CRUDBase[BizRefund]):
+ """退款记录CRUD操作"""
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ region_id: str = None,
+ order_id: str = None,
+ order_no: str = None,
+ status: str = None,
+ refund_method: str = None,
+ start_date: str = None,
+ end_date: str = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> Tuple[List[BizRefund], int]:
+ """多条件查询退款记录"""
+ query = db.query(BizRefund).filter(BizRefund.is_del == 0)
+
+ if region_id:
+ # 需要关联订单获取区域
+ query = query.join(BizOrder, BizOrder.id == BizRefund.order_id).filter(
+ BizOrder.region_id == region_id
+ )
+ if order_id:
+ query = query.filter(BizRefund.order_id == order_id)
+ if order_no:
+ query = query.join(BizOrder, BizOrder.id == BizRefund.order_id).filter(
+ BizOrder.order_no.like(f"%{order_no}%")
+ )
+ if status:
+ query = query.filter(BizRefund.status == status)
+ if refund_method:
+ query = query.filter(BizRefund.refund_method == refund_method)
+ if start_date:
+ query = query.filter(BizRefund.create_at >= start_date)
+ if end_date:
+ query = query.filter(BizRefund.create_at <= end_date)
+
+ total = query.count()
+ refunds = query.order_by(BizRefund.create_at.desc()).offset(skip).limit(limit).all()
+ return refunds, total
+
+ def get_by_order(self, db: Session, order_id: str) -> List[BizRefund]:
+ """根据订单获取退款记录"""
+ return db.query(BizRefund).filter(
+ and_(
+ BizRefund.order_id == order_id,
+ BizRefund.is_del == 0
+ )
+ ).all()
+
+ def get_by_refund_no(self, db: Session, refund_no: str) -> Optional[BizRefund]:
+ """根据退款单号获取退款记录"""
+ return db.query(BizRefund).filter(
+ and_(
+ BizRefund.refund_no == refund_no,
+ BizRefund.is_del == 0
+ )
+ ).first()
+
+ def create_refund(
+ self,
+ db: Session,
+ order_id: str,
+ refund_amount: Decimal,
+ refund_method: str,
+ reason: str = None,
+ creator: dict = None
+ ) -> BizRefund:
+ """创建退款记录"""
+ refund_no = self._generate_refund_no(db)
+
+ refund = BizRefund(
+ id=generate_id(),
+ order_id=order_id,
+ refund_no=refund_no,
+ refund_amount=refund_amount,
+ refund_method=refund_method,
+ refund_result="processing",
+ status="pending",
+ create_by=creator.get("user_id") if creator else None,
+ create_name=creator.get("real_name") if creator else None
+ )
+ db.add(refund)
+
+ # 更新订单的退款金额和状态
+ order = db.query(BizOrder).filter(BizOrder.id == order_id).first()
+ if order:
+ order.refund_amount = (order.refund_amount or Decimal("0")) + refund_amount
+ order.update_at = datetime.now()
+
+ # 如果是微信退款,更新订单状态为退款中
+ if refund_method == "wechat":
+ order.pay_status = "refunding"
+ order.status = derive_legacy_status_from_three(
+ order_status=order.order_status,
+ delivery_status=order.delivery_status,
+ pay_status=order.pay_status,
+ )
+
+ db.commit()
+ db.refresh(refund)
+ return refund
+
+ def process_refund(
+ self,
+ db: Session,
+ refund_id: str,
+ refund_result: str,
+ refund_no_wechat: str = None,
+ updater: dict = None
+ ) -> Optional[BizRefund]:
+ """处理退款(成功/失败)"""
+ refund = self.get(db, refund_id)
+ if not refund:
+ return None
+
+ refund.refund_result = refund_result
+ refund.refund_time = datetime.now()
+ if refund_no_wechat:
+ refund.refund_no_wechat = refund_no_wechat
+
+ if refund_result == "success":
+ refund.status = "completed"
+ # 更新订单退款金额
+ order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first()
+ if order:
+ order.refund_amount = (order.refund_amount or Decimal("0")) + refund.refund_amount
+ order.paid_amount = max(Decimal("0"), (order.paid_amount or Decimal("0")) - refund.refund_amount)
+ order.pay_status = "refunded" if (order.paid_amount or Decimal("0")) <= Decimal("0") else "paid"
+ order.status = derive_legacy_status_from_three(
+ order_status=order.order_status,
+ delivery_status=order.delivery_status,
+ pay_status=order.pay_status,
+ )
+ order.update_at = datetime.now()
+
+ # 如果退款金额等于已支付金额,订单变为已取消
+ if order.refund_amount >= (order.total_amount - order.paid_amount + order.refund_amount):
+ # 保留部分退款状态
+ pass
+ else:
+ refund.status = "failed"
+ # 恢复订单状态
+ order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first()
+ if order and order.pay_status == "refunding":
+ order.pay_status = "paid" if (order.paid_amount or Decimal("0")) > Decimal("0") else "pending_payment"
+ order.status = derive_legacy_status_from_three(
+ order_status=order.order_status,
+ delivery_status=order.delivery_status,
+ pay_status=order.pay_status,
+ )
+
+ refund.update_at = datetime.now()
+ db.commit()
+ db.refresh(refund)
+ return refund
+
+ def handle_refund_failure(
+ self,
+ db: Session,
+ refund_id: str,
+ fail_reason: str
+ ) -> Optional[BizRefund]:
+ """处理退款失败"""
+ refund = self.get(db, refund_id)
+ if not refund:
+ return None
+
+ refund.refund_result = "failed"
+ refund.refund_fail_reason = fail_reason
+ refund.refund_time = datetime.now()
+ refund.status = "failed"
+ refund.update_at = datetime.now()
+
+ # 恢复订单状态
+ order = db.query(BizOrder).filter(BizOrder.id == refund.order_id).first()
+ if order and order.pay_status == "refunding":
+ order.pay_status = "paid" if (order.paid_amount or Decimal("0")) > Decimal("0") else "pending_payment"
+ order.status = derive_legacy_status_from_three(
+ order_status=order.order_status,
+ delivery_status=order.delivery_status,
+ pay_status=order.pay_status,
+ )
+ order.update_at = datetime.now()
+
+ db.commit()
+ db.refresh(refund)
+ return refund
+
+ def _generate_refund_no(self, db: Session) -> str:
+ """生成退款单号"""
+ today = datetime.now().strftime("%Y%m%d")
+ prefix = f"REF{today}"
+
+ last_refund = db.query(BizRefund).filter(
+ BizRefund.refund_no.like(f"{prefix}%")
+ ).order_by(BizRefund.refund_no.desc()).first()
+
+ if last_refund:
+ last_no = int(last_refund.refund_no[-4:])
+ new_no = last_no + 1
+ else:
+ new_no = 1
+
+ return f"{prefix}{str(new_no).zfill(4)}"
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BizRefund]:
+ """更新退款记录并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "order_id": original.order_id,
+ "refund_no": original.refund_no,
+ "refund_amount": original.refund_amount,
+ "refund_method": original.refund_method,
+ "refund_no_wechat": original.refund_no_wechat,
+ "refund_time": original.refund_time,
+ "refund_result": original.refund_result,
+ "refund_fail_reason": original.refund_fail_reason,
+ "status": original.status,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BizRefundHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def get_refund_statistics(
+ self,
+ db: Session,
+ start_date: str = None,
+ end_date: str = None
+ ) -> dict:
+ """获取退款统计数据"""
+ base_query = db.query(BizRefund).filter(
+ and_(
+ BizRefund.is_del == 0,
+ BizRefund.status == "completed"
+ )
+ )
+
+ if start_date:
+ base_query = base_query.filter(BizRefund.refund_time >= start_date)
+ if end_date:
+ base_query = base_query.filter(BizRefund.refund_time <= end_date)
+
+ # 成功退款金额
+ success_amount = base_query.filter(
+ BizRefund.refund_result == "success"
+ ).func.sum(BizRefund.refund_amount).scalar() or Decimal("0")
+
+ # 失败退款金额
+ failed_amount = base_query.filter(
+ BizRefund.refund_result == "failed"
+ ).func.sum(BizRefund.refund_amount).scalar() or Decimal("0")
+
+ # 待处理退款数
+ pending_count = db.query(func.count(BizRefund.id)).filter(
+ and_(
+ BizRefund.is_del == 0,
+ BizRefund.status.in_(["pending", "processing"])
+ )
+ ).scalar() or 0
+
+ # 退款失败数
+ failed_count = db.query(func.count(BizRefund.id)).filter(
+ and_(
+ BizRefund.is_del == 0,
+ BizRefund.status == "failed"
+ )
+ ).scalar() or 0
+
+ return {
+ "successAmount": float(success_amount),
+ "failedAmount": float(failed_amount),
+ "pendingCount": pending_count,
+ "failedCount": failed_count
+ }
+
+
+settlement_crud = SettlementCRUD(BizSettlement)
+refund_crud = RefundCRUD(BizRefund)
diff --git a/backend/app/crud/statistics.py b/backend/app/crud/statistics.py
new file mode 100644
index 00000000..1ffe41d9
--- /dev/null
+++ b/backend/app/crud/statistics.py
@@ -0,0 +1,916 @@
+"""数据统计 CRUD"""
+from sqlalchemy.orm import Session
+from sqlalchemy import and_, func, desc, extract
+from typing import Optional, List, Dict, Any, Tuple
+from datetime import datetime, timedelta
+from decimal import Decimal
+
+from app.models.models import (
+ BizOrder, BizOrderItem, BizDeliveryTask,
+ BizSettlement, BizRefund, BizPriceList, BizPriceListItem,
+ SysUser, SysRegion, BasProduct,
+ BizInquiry, BizQuotation, BizDeliveryWarning,
+)
+
+
+class StatisticsCRUD:
+ """数据统计CRUD操作"""
+
+ @staticmethod
+ def get_order_statistics(
+ db: Session,
+ region_id: str = None,
+ start_date: str = None,
+ end_date: str = None
+ ) -> Dict[str, Any]:
+ """获取订单统计数据"""
+ query = db.query(BizOrder).filter(BizOrder.is_del == 0)
+
+ if region_id:
+ query = query.filter(BizOrder.region_id == region_id)
+
+ if start_date:
+ try:
+ start_dt = datetime.strptime(start_date, "%Y-%m-%d")
+ query = query.filter(BizOrder.create_at >= start_dt)
+ except:
+ pass
+
+ if end_date:
+ try:
+ end_dt = datetime.strptime(end_date, "%Y-%m-%d")
+ end_dt = end_dt.replace(hour=23, minute=59, second=59)
+ query = query.filter(BizOrder.create_at <= end_dt)
+ except:
+ pass
+
+ orders = query.all()
+
+ total_orders = len(orders)
+ pending_payment = sum(1 for o in orders if o.status == "pending_payment")
+ pending_dispatch = sum(1 for o in orders if getattr(o, "delivery_status", None) == "pending_dispatch")
+ delivering = sum(1 for o in orders if o.status == "delivering")
+ completed = sum(1 for o in orders if o.status == "completed")
+ cancelled = sum(1 for o in orders if o.status == "cancelled")
+
+ total_amount = sum(float(o.total_amount or 0) for o in orders)
+ paid_amount = sum(float(o.paid_amount or 0) for o in orders)
+ refund_amount = sum(float(o.refund_amount or 0) for o in orders)
+
+ return {
+ "totalOrders": total_orders,
+ "pendingPayment": pending_payment,
+ "pendingDelivery": pending_dispatch,
+ "delivering": delivering,
+ "completed": completed,
+ "cancelled": cancelled,
+ "totalAmount": round(total_amount, 2),
+ "paidAmount": round(paid_amount, 2),
+ "refundAmount": round(refund_amount, 2)
+ }
+
+ @staticmethod
+ def get_order_trend(
+ db: Session,
+ region_id: str = None,
+ days: int = 7,
+ include_inactive_regions: bool = False,
+ ) -> List[Dict[str, Any]]:
+ """获取订单趋势数据(按日统计)"""
+ result = []
+ today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+ region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
+
+ for i in range(days - 1, -1, -1):
+ day_start = today - timedelta(days=i)
+ day_end = day_start + timedelta(days=1)
+
+ query = (
+ db.query(BizOrder)
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.create_at >= day_start,
+ BizOrder.create_at < day_end,
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ )
+ )
+ )
+
+ if region_id:
+ query = query.filter(BizOrder.region_id == region_id)
+
+ orders = query.all()
+ order_count = len(orders)
+ order_amount = sum(float(o.total_amount or 0) for o in orders)
+
+ result.append({
+ "date": day_start.strftime("%Y-%m-%d"),
+ "orderCount": order_count,
+ "orderAmount": round(order_amount, 2)
+ })
+
+ return result
+
+ @staticmethod
+ def get_today_order_stats_by_region(
+ db: Session,
+ region_id: str = None,
+ include_inactive_regions: bool = False,
+ ) -> List[Dict[str, Any]]:
+ """今日各区域营业额(元)、订单笔数;列出未删除的区域,无单则为 0。"""
+ today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+ day_end = today + timedelta(days=1)
+ region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
+
+ reg_q = db.query(SysRegion).filter(
+ SysRegion.is_del == 0,
+ )
+ if region_id:
+ reg_q = reg_q.filter(SysRegion.id == region_id)
+ regions = reg_q.order_by(SysRegion.sort_order, SysRegion.name).all()
+
+ agg_q = (
+ db.query(
+ BizOrder.region_id,
+ func.count(BizOrder.id).label("order_count"),
+ func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"),
+ )
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(
+ BizOrder.is_del == 0,
+ BizOrder.create_at >= today,
+ BizOrder.create_at < day_end,
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ )
+ .group_by(BizOrder.region_id)
+ )
+ if region_id:
+ agg_q = agg_q.filter(BizOrder.region_id == region_id)
+ agg_map: Dict[str, Any] = {r.region_id: r for r in agg_q.all() if r.region_id}
+
+ out: List[Dict[str, Any]] = []
+ for reg in regions:
+ row = agg_map.get(reg.id)
+ cnt = int(row.order_count or 0) if row else 0
+ amt = float(row.order_amount or 0) if row else 0.0
+ out.append({
+ "regionId": reg.id,
+ "regionName": reg.name or reg.id,
+ "orderCount": cnt,
+ "orderAmount": round(amt, 2),
+ })
+ return out
+
+ @staticmethod
+ def get_today_order_stats_by_client(
+ db: Session,
+ region_id: str = None,
+ limit: int = 30,
+ include_inactive_regions: bool = False,
+ ) -> List[Dict[str, Any]]:
+ """今日各下单客户:下单金额合计(元)、商品数量合计(订单行数量之和);按金额降序,最多 limit 条。"""
+ today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+ day_end = today + timedelta(days=1)
+ region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
+
+ amt_q = (
+ db.query(
+ BizOrder.client_id,
+ func.coalesce(func.sum(BizOrder.total_amount), 0).label("order_amount"),
+ )
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(
+ BizOrder.is_del == 0,
+ BizOrder.create_at >= today,
+ BizOrder.create_at < day_end,
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ )
+ )
+ if region_id:
+ amt_q = amt_q.filter(BizOrder.region_id == region_id)
+ amt_rows = amt_q.group_by(BizOrder.client_id).all()
+
+ qty_q = (
+ db.query(
+ BizOrder.client_id,
+ func.coalesce(func.sum(BizOrderItem.quantity), 0).label("qty"),
+ )
+ .select_from(BizOrderItem)
+ .join(
+ BizOrder,
+ and_(
+ BizOrder.id == BizOrderItem.order_id,
+ BizOrder.is_del == 0,
+ BizOrder.create_at >= today,
+ BizOrder.create_at < day_end,
+ ),
+ )
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(BizOrderItem.is_del == 0)
+ .filter(SysRegion.is_del == 0, *region_active_clause)
+ )
+ if region_id:
+ qty_q = qty_q.filter(BizOrder.region_id == region_id)
+ qty_rows = qty_q.group_by(BizOrder.client_id).all()
+ qty_map: Dict[str, float] = {
+ r.client_id: float(r.qty or 0) for r in qty_rows if r.client_id
+ }
+
+ merged: List[Dict[str, Any]] = []
+ for r in amt_rows:
+ cid = r.client_id
+ if not cid:
+ continue
+ merged.append({
+ "clientId": cid,
+ "orderAmount": round(float(r.order_amount or 0), 2),
+ "productQuantity": round(float(qty_map.get(cid, 0) or 0), 4),
+ })
+
+ merged.sort(key=lambda x: -x["orderAmount"])
+ merged = merged[: max(1, min(int(limit or 30), 100))]
+
+ client_ids = [m["clientId"] for m in merged]
+ names: Dict[str, str] = {}
+ if client_ids:
+ for u in (
+ db.query(SysUser)
+ .filter(and_(SysUser.is_del == 0, SysUser.id.in_(client_ids)))
+ .all()
+ ):
+ names[u.id] = (u.real_name or u.phone or u.username or u.id) or u.id
+
+ out: List[Dict[str, Any]] = []
+ for m in merged:
+ cid = m["clientId"]
+ nm = names.get(cid, cid)
+ out.append({
+ "clientId": cid,
+ "clientName": nm,
+ "orderAmount": m["orderAmount"],
+ "productQuantity": m["productQuantity"],
+ })
+ return out
+
+ @staticmethod
+ def get_financial_statistics(
+ db: Session,
+ region_id: str = None,
+ start_date: str = None,
+ end_date: str = None
+ ) -> Dict[str, Any]:
+ """获取财务统计数据"""
+ order_query = db.query(BizOrder).filter(BizOrder.is_del == 0)
+ settlement_query = db.query(BizSettlement).filter(BizSettlement.is_del == 0)
+ refund_query = db.query(BizRefund).filter(BizRefund.is_del == 0)
+
+ if region_id:
+ order_query = order_query.filter(BizOrder.region_id == region_id)
+ settlement_query = settlement_query.filter(BizSettlement.region_id == region_id)
+
+ if start_date:
+ try:
+ start_dt = datetime.strptime(start_date, "%Y-%m-%d")
+ order_query = order_query.filter(BizOrder.create_at >= start_dt)
+ settlement_query = settlement_query.filter(BizSettlement.create_at >= start_dt)
+ refund_query = refund_query.filter(BizRefund.create_at >= start_dt)
+ except:
+ pass
+
+ if end_date:
+ try:
+ end_dt = datetime.strptime(end_date, "%Y-%m-%d")
+ end_dt = end_dt.replace(hour=23, minute=59, second=59)
+ order_query = order_query.filter(BizOrder.create_at <= end_dt)
+ settlement_query = settlement_query.filter(BizSettlement.create_at <= end_dt)
+ refund_query = refund_query.filter(BizRefund.create_at <= end_dt)
+ except:
+ pass
+
+ orders = order_query.all()
+ settlements = settlement_query.filter(BizSettlement.status == "confirmed").all()
+ refunds = refund_query.filter(BizRefund.status == "completed").all()
+
+ total_revenue = sum(float(o.total_amount or 0) for o in orders)
+ total_received = sum(float(s.amount or 0) for s in settlements)
+ total_refund = sum(float(r.refund_amount or 0) for r in refunds)
+ net_revenue = total_received - total_refund
+
+ return {
+ "totalRevenue": round(total_revenue, 2),
+ "totalReceived": round(total_received, 2),
+ "totalRefund": round(total_refund, 2),
+ "netRevenue": round(net_revenue, 2),
+ "orderCount": len(orders),
+ "settlementCount": len(settlements),
+ "refundCount": len(refunds)
+ }
+
+ @staticmethod
+ def get_financial_trend(
+ db: Session,
+ region_id: str = None,
+ days: int = 7
+ ) -> List[Dict[str, Any]]:
+ """获取财务趋势数据(按日统计)"""
+ result = []
+ today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+
+ for i in range(days - 1, -1, -1):
+ day_start = today - timedelta(days=i)
+ day_end = day_start + timedelta(days=1)
+
+ settlement_query = db.query(BizSettlement).filter(
+ and_(
+ BizSettlement.is_del == 0,
+ BizSettlement.status == "confirmed",
+ BizSettlement.create_at >= day_start,
+ BizSettlement.create_at < day_end
+ )
+ )
+
+ refund_query = db.query(BizRefund).filter(
+ and_(
+ BizRefund.is_del == 0,
+ BizRefund.status == "completed",
+ BizRefund.create_at >= day_start,
+ BizRefund.create_at < day_end
+ )
+ )
+
+ if region_id:
+ settlement_query = settlement_query.filter(BizSettlement.region_id == region_id)
+
+ settlements = settlement_query.all()
+ refunds = refund_query.all()
+
+ received = sum(float(s.amount or 0) for s in settlements)
+ refund = sum(float(r.refund_amount or 0) for r in refunds)
+
+ result.append({
+ "date": day_start.strftime("%Y-%m-%d"),
+ "received": round(received, 2),
+ "refund": round(refund, 2),
+ "net": round(received - refund, 2)
+ })
+
+ return result
+
+ @staticmethod
+ def get_delivery_statistics(
+ db: Session,
+ region_id: str = None,
+ start_date: str = None,
+ end_date: str = None
+ ) -> Dict[str, Any]:
+ """获取配送统计数据"""
+ query = db.query(BizDeliveryTask).filter(BizDeliveryTask.is_del == 0)
+
+ if region_id:
+ query = query.filter(BizDeliveryTask.region_id == region_id)
+
+ if start_date:
+ try:
+ start_dt = datetime.strptime(start_date, "%Y-%m-%d")
+ query = query.filter(BizDeliveryTask.create_at >= start_dt)
+ except:
+ pass
+
+ if end_date:
+ try:
+ end_dt = datetime.strptime(end_date, "%Y-%m-%d")
+ end_dt = end_dt.replace(hour=23, minute=59, second=59)
+ query = query.filter(BizDeliveryTask.create_at <= end_dt)
+ except:
+ pass
+
+ tasks = query.all()
+
+ pending = sum(1 for t in tasks if t.status == "pending")
+ picked_up = sum(1 for t in tasks if t.status == "picked_up")
+ delivering = sum(1 for t in tasks if t.status == "delivering")
+ completed = sum(1 for t in tasks if t.status == "completed")
+ cancelled = sum(1 for t in tasks if t.status == "cancelled")
+
+ return {
+ "totalTasks": len(tasks),
+ "pending": pending,
+ "pickedUp": picked_up,
+ "delivering": delivering,
+ "completed": completed,
+ "cancelled": cancelled
+ }
+
+ @staticmethod
+ def get_product_statistics(
+ db: Session,
+ region_id: str = None,
+ start_date: str = None,
+ end_date: str = None,
+ limit: int = 10
+ ) -> List[Dict[str, Any]]:
+ """获取商品销售排行"""
+ query = db.query(
+ BizOrderItem.product_id,
+ func.sum(BizOrderItem.quantity).label("total_quantity"),
+ func.sum(BizOrderItem.amount).label("total_amount")
+ ).join(
+ BizOrder,
+ and_(
+ BizOrder.id == BizOrderItem.order_id,
+ BizOrder.is_del == 0,
+ BizOrder.status.in_(["completed", "delivering", "pending_confirm"])
+ )
+ ).filter(
+ BizOrderItem.is_del == 0
+ )
+
+ if region_id:
+ query = query.filter(BizOrder.region_id == region_id)
+
+ if start_date:
+ try:
+ start_dt = datetime.strptime(start_date, "%Y-%m-%d")
+ query = query.filter(BizOrder.create_at >= start_dt)
+ except:
+ pass
+
+ if end_date:
+ try:
+ end_dt = datetime.strptime(end_date, "%Y-%m-%d")
+ end_dt = end_dt.replace(hour=23, minute=59, second=59)
+ query = query.filter(BizOrder.create_at <= end_dt)
+ except:
+ pass
+
+ results = query.group_by(BizOrderItem.product_id).order_by(
+ desc("total_amount")
+ ).limit(limit).all()
+
+ product_stats = []
+ for r in results:
+ product = db.query(BasProduct).filter(
+ and_(BasProduct.id == r.product_id, BasProduct.is_del == 0)
+ ).first()
+
+ product_stats.append({
+ "productId": r.product_id,
+ "productName": product.name if product else None,
+ "totalQuantity": float(r.total_quantity or 0),
+ "totalAmount": float(r.total_amount or 0)
+ })
+
+ return product_stats
+
+ @staticmethod
+ def get_customer_statistics(
+ db: Session,
+ region_id: str = None,
+ start_date: str = None,
+ end_date: str = None,
+ limit: int = 10
+ ) -> List[Dict[str, Any]]:
+ """获取客户订单排行"""
+ query = db.query(
+ BizOrder.client_id,
+ func.count(BizOrder.id).label("order_count"),
+ func.sum(BizOrder.total_amount).label("total_amount")
+ ).filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.status.in_(["completed", "delivering", "pending_confirm", "pending_payment"])
+ )
+ )
+
+ if region_id:
+ query = query.filter(BizOrder.region_id == region_id)
+
+ if start_date:
+ try:
+ start_dt = datetime.strptime(start_date, "%Y-%m-%d")
+ query = query.filter(BizOrder.create_at >= start_dt)
+ except:
+ pass
+
+ if end_date:
+ try:
+ end_dt = datetime.strptime(end_date, "%Y-%m-%d")
+ end_dt = end_dt.replace(hour=23, minute=59, second=59)
+ query = query.filter(BizOrder.create_at <= end_dt)
+ except:
+ pass
+
+ results = query.group_by(BizOrder.client_id).order_by(
+ desc("total_amount")
+ ).limit(limit).all()
+
+ customer_stats = []
+ for r in results:
+ customer = db.query(SysUser).filter(
+ and_(SysUser.id == r.client_id, SysUser.is_del == 0)
+ ).first()
+
+ customer_stats.append({
+ "customerId": r.client_id,
+ "customerName": customer.real_name if customer else None,
+ "customerPhone": customer.phone if customer else None,
+ "orderCount": r.order_count,
+ "totalAmount": float(r.total_amount or 0)
+ })
+
+ return customer_stats
+
+ @staticmethod
+ def _dashboard_order_region_clause(region_id: str = None):
+ return [BizOrder.region_id == region_id] if region_id else []
+
+ @staticmethod
+ def get_dashboard_summary(
+ db: Session,
+ region_id: str = None,
+ include_inactive_regions: bool = False,
+ ) -> Dict[str, Any]:
+ """获取仪表盘汇总数据(运营工作台;含待办摘要、最新订单、预警数等,字段驼峰)"""
+ today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
+ tomorrow_start = today + timedelta(days=1)
+ month_start = today.replace(day=1)
+ yesterday_start = today - timedelta(days=1)
+ region_clause = StatisticsCRUD._dashboard_order_region_clause(region_id)
+ region_active_clause = [] if include_inactive_regions else [SysRegion.is_active == True]
+
+ order_query = (
+ db.query(BizOrder)
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.create_at >= month_start,
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *region_clause,
+ )
+ )
+ )
+ month_orders = order_query.all()
+ today_orders = [
+ o for o in month_orders if o.create_at and o.create_at >= today
+ ]
+ prev_day_orders = [
+ o
+ for o in month_orders
+ if o.create_at and yesterday_start <= o.create_at < today
+ ]
+
+ pending_delivery_q = (
+ db.query(BizOrder)
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.delivery_status == "pending_dispatch",
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *region_clause,
+ )
+ )
+ )
+ pending_delivery_count = pending_delivery_q.count()
+
+ def _delta_hint(cur_n: int, prev_n: int, unit: str) -> str:
+ d = cur_n - prev_n
+ if d > 0:
+ return f"较昨日 +{d}{unit}"
+ if d < 0:
+ return f"较昨日 {d}{unit}"
+ return "与昨日持平"
+
+ def _amount_delta_hint(cur_amt: float, prev_amt: float) -> str:
+ d = round(cur_amt - prev_amt, 2)
+ if d > 0.0001:
+ return f"较昨日 +¥{d:,.2f}"
+ if d < -0.0001:
+ return f"较昨日 ¥{d:,.2f}"
+ return "与昨日持平"
+
+ today_order_count = len(today_orders)
+ today_order_amount = round(sum(float(o.total_amount or 0) for o in today_orders), 2)
+ prev_count = len(prev_day_orders)
+ prev_amount = round(sum(float(o.total_amount or 0) for o in prev_day_orders), 2)
+
+ # 未解决的配送时效预警(可按区域通过任务表关联)
+ warn_q = (
+ db.query(BizDeliveryWarning)
+ .join(
+ BizDeliveryTask,
+ and_(
+ BizDeliveryTask.id == BizDeliveryWarning.task_id,
+ BizDeliveryTask.is_del == 0,
+ ),
+ )
+ .join(SysRegion, BizDeliveryTask.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizDeliveryWarning.is_del == 0,
+ BizDeliveryWarning.is_resolved == 0,
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ )
+ )
+ )
+ if region_id:
+ warn_q = warn_q.filter(BizDeliveryTask.region_id == region_id)
+ delivery_warning_count = warn_q.count()
+
+ # 今日新提交报价:状态为已提交,且提交时间(无则用创建时间)落在当日 0 点~次日 0 点
+ quot_submitted_at = func.coalesce(BizQuotation.submit_time, BizQuotation.create_at)
+ sub_quot_q = (
+ db.query(BizQuotation)
+ .join(SysRegion, BizQuotation.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizQuotation.is_del == 0,
+ BizQuotation.status == "submitted",
+ quot_submitted_at >= today,
+ quot_submitted_at < tomorrow_start,
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ )
+ )
+ )
+ if region_id:
+ sub_quot_q = sub_quot_q.filter(BizQuotation.region_id == region_id)
+ pending_quotation_count = sub_quot_q.count()
+
+ sub_quot_preview = (
+ db.query(BizQuotation, BizInquiry)
+ .join(BizInquiry, BizInquiry.id == BizQuotation.inquiry_id)
+ .join(SysRegion, BizQuotation.region_id == SysRegion.id)
+ .filter(
+ BizQuotation.is_del == 0,
+ BizQuotation.status == "submitted",
+ quot_submitted_at >= today,
+ quot_submitted_at < tomorrow_start,
+ BizInquiry.is_del == 0,
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *([BizQuotation.region_id == region_id] if region_id else []),
+ )
+ .order_by(desc(BizQuotation.submit_time), desc(BizQuotation.create_at))
+ .limit(2)
+ .all()
+ )
+ quotation_lines = []
+ for q, inq in sub_quot_preview:
+ title = (inq.title or "询价单")[:48]
+ quotation_lines.append(title)
+
+ pd_preview = (
+ db.query(BizOrder)
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.delivery_status == "pending_dispatch",
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *region_clause,
+ )
+ )
+ .order_by(desc(BizOrder.create_at))
+ .limit(2)
+ .all()
+ )
+ delivery_lines = [o.order_no for o in pd_preview]
+
+ cancel_preview = (
+ db.query(BizOrder)
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.cancel_status == "pending_review",
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *region_clause,
+ )
+ )
+ .order_by(desc(BizOrder.update_at))
+ .limit(2)
+ .all()
+ )
+ cancel_client_ids = list({o.client_id for o in cancel_preview if o.client_id})
+ cancel_clients = {}
+ if cancel_client_ids:
+ for u in (
+ db.query(SysUser)
+ .filter(and_(SysUser.is_del == 0, SysUser.id.in_(cancel_client_ids)))
+ .all()
+ ):
+ cancel_clients[u.id] = u.real_name or u.phone or u.id
+ cancel_lines = []
+ for o in cancel_preview:
+ nm = cancel_clients.get(o.client_id, "")
+ cancel_lines.append(f"{o.order_no}" + (f" · {nm[:16]}" if nm else ""))
+
+ settle_preview = (
+ db.query(BizOrder)
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.client_settlement_status == "pending",
+ BizOrder.pay_status == "paid",
+ BizOrder.order_status.in_(["pending_confirm", "completed"]),
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *region_clause,
+ )
+ )
+ .order_by(desc(BizOrder.update_at))
+ .limit(2)
+ .all()
+ )
+ settle_client_ids = list({o.client_id for o in settle_preview if o.client_id})
+ settle_clients = {}
+ if settle_client_ids:
+ for u in (
+ db.query(SysUser)
+ .filter(and_(SysUser.is_del == 0, SysUser.id.in_(settle_client_ids)))
+ .all()
+ ):
+ settle_clients[u.id] = u.real_name or u.phone or u.id
+ settle_lines = []
+ for o in settle_preview:
+ nm = settle_clients.get(o.client_id, "")
+ settle_lines.append(f"{o.order_no}" + (f" · {nm[:16]}" if nm else ""))
+
+ pending_settlement_count = (
+ db.query(BizOrder)
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.client_settlement_status == "pending",
+ BizOrder.pay_status == "paid",
+ BizOrder.order_status.in_(["pending_confirm", "completed"]),
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *region_clause,
+ )
+ )
+ .count()
+ )
+
+ pending_cancel_count = (
+ db.query(BizOrder)
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizOrder.is_del == 0,
+ BizOrder.cancel_status == "pending_review",
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *region_clause,
+ )
+ )
+ .count()
+ )
+
+ # 最新订单(工作台列表)
+ recent_rows = (
+ db.query(BizOrder)
+ .join(SysRegion, BizOrder.region_id == SysRegion.id)
+ .filter(and_(BizOrder.is_del == 0, SysRegion.is_del == 0, *region_active_clause, *region_clause))
+ .order_by(desc(BizOrder.create_at))
+ .limit(8)
+ .all()
+ )
+ rid_set = list({o.region_id for o in recent_rows if o.region_id})
+ cid_set = list({o.client_id for o in recent_rows if o.client_id})
+ region_name_map = {}
+ if rid_set:
+ for r in db.query(SysRegion).filter(
+ and_(SysRegion.is_del == 0, *region_active_clause, SysRegion.id.in_(rid_set))
+ ).all():
+ region_name_map[r.id] = r.name
+ client_name_map = {}
+ if cid_set:
+ for u in db.query(SysUser).filter(and_(SysUser.is_del == 0, SysUser.id.in_(cid_set))).all():
+ client_name_map[u.id] = u.real_name or u.phone or "-"
+
+ recent_orders = []
+ for o in recent_rows:
+ order_status = getattr(o, "order_status", None) or "pending_payment"
+ recent_orders.append(
+ {
+ "id": o.id,
+ "orderNo": o.order_no,
+ "clientName": client_name_map.get(o.client_id, "-"),
+ "regionName": region_name_map.get(o.region_id, "-"),
+ "totalAmount": float(o.total_amount) if o.total_amount else 0,
+ "orderStatus": order_status,
+ "cancelStatus": getattr(o, "cancel_status", None) or "none",
+ "deliveryStatus": getattr(o, "delivery_status", None) or "pending_dispatch",
+ "createAt": str(o.create_at) if o.create_at else None,
+ }
+ )
+
+ todo_snapshots = [
+ {
+ "type": "quotation",
+ "title": "今日新提交报价",
+ "count": pending_quotation_count,
+ "items": quotation_lines,
+ },
+ {
+ "type": "delivery",
+ "title": "待分配配送",
+ "count": pending_delivery_count,
+ "items": delivery_lines,
+ },
+ {
+ "type": "cancel",
+ "title": "取消待审核",
+ "count": pending_cancel_count,
+ "items": cancel_lines,
+ },
+ {
+ "type": "settlement",
+ "title": "待客户结算",
+ "count": pending_settlement_count,
+ "items": settle_lines,
+ },
+ ]
+
+ # “客户总数/供应商总数”口径:统计用户表(sys_user)并按 region_id 区分
+ # - region_id 为空:不过滤全局
+ # - region_id 传入:按 SysUser.region_id 过滤
+ customer_count = (
+ db.query(SysUser)
+ .join(SysRegion, SysUser.region_id == SysRegion.id)
+ .filter(
+ and_(
+ SysUser.is_del == 0,
+ SysUser.role == "client",
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *([SysUser.region_id == region_id] if region_id else []),
+ )
+ )
+ .count()
+ )
+
+ supplier_count = (
+ db.query(SysUser)
+ .join(SysRegion, SysUser.region_id == SysRegion.id)
+ .filter(
+ and_(
+ SysUser.is_del == 0,
+ SysUser.role == "supplier",
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *([SysUser.region_id == region_id] if region_id else []),
+ )
+ )
+ .count()
+ )
+
+ return {
+ "todayOrderCount": today_order_count,
+ "todayOrderAmount": today_order_amount,
+ "monthOrderCount": len(month_orders),
+ "monthOrderAmount": round(sum(float(o.total_amount or 0) for o in month_orders), 2),
+ "pendingDeliveryCount": pending_delivery_count,
+ "completedOrderCount": sum(
+ 1 for o in month_orders if (getattr(o, "order_status", None) or o.status) == "completed"
+ ),
+ "activePriceListCount": (
+ db.query(BizPriceList)
+ .join(SysRegion, BizPriceList.region_id == SysRegion.id)
+ .filter(
+ and_(
+ BizPriceList.is_del == 0,
+ BizPriceList.status == "active",
+ SysRegion.is_del == 0,
+ *region_active_clause,
+ *([BizPriceList.region_id == region_id] if region_id else []),
+ )
+ )
+ .count()
+ ),
+ "customerCount": customer_count,
+ "supplierCount": supplier_count,
+ "todayOrderCountHint": _delta_hint(today_order_count, prev_count, " 单"),
+ "todayOrderAmountHint": _amount_delta_hint(today_order_amount, prev_amount),
+ "deliveryWarningCount": delivery_warning_count,
+ "recentOrders": recent_orders,
+ "todoSnapshots": todo_snapshots,
+ }
+
+
+statistics_crud = StatisticsCRUD()
diff --git a/backend/app/crud/unit.py b/backend/app/crud/unit.py
new file mode 100644
index 00000000..1dba9ceb
--- /dev/null
+++ b/backend/app/crud/unit.py
@@ -0,0 +1,72 @@
+"""商品单位CRUD"""
+from typing import Optional, List
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from app.models.models import BasProductUnit, BasProductUnitHis
+from app.crud.base import CRUDBase
+
+
+class UnitCRUD(CRUDBase[BasProductUnit]):
+ """商品单位CRUD操作"""
+
+ def get_by_code(self, db: Session, code: str) -> Optional[BasProductUnit]:
+ """根据编码获取单位"""
+ return db.query(BasProductUnit).filter(
+ and_(BasProductUnit.code == code, BasProductUnit.is_del == 0)
+ ).first()
+
+ def get_active_all(self, db: Session) -> List[BasProductUnit]:
+ """获取所有启用的单位"""
+ return db.query(BasProductUnit).filter(
+ and_(BasProductUnit.is_del == 0, BasProductUnit.is_active == True)
+ ).order_by(BasProductUnit.sort_order.asc(), BasProductUnit.id.asc()).all()
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[BasProductUnit]:
+ """更新单位并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "name": original.name,
+ "code": original.code,
+ "is_active": original.is_active,
+ "sort_order": original.sort_order,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BasProductUnitHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, id: str) -> bool:
+ """删除单位并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return False
+
+ his_data = {
+ "id": original.id,
+ "name": original.name,
+ "code": original.code,
+ "is_active": original.is_active,
+ "sort_order": original.sort_order,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = BasProductUnitHis(**his_data)
+ db.add(his)
+
+ return self.delete(db, id)
+
+
+unit_crud = UnitCRUD(BasProductUnit)
diff --git a/backend/app/crud/user.py b/backend/app/crud/user.py
new file mode 100644
index 00000000..6d2bbcf9
--- /dev/null
+++ b/backend/app/crud/user.py
@@ -0,0 +1,177 @@
+"""用户CRUD"""
+from typing import Optional, List, Tuple
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from app.models.models import SysUser, SysUserHis
+from app.crud.base import CRUDBase
+from app.core.security import get_password_hash
+from app.core.utils import generate_random_password
+
+# 运营端「重置密码」专用明文:仅服务端使用,禁止由 HTTP 请求传入
+DEFAULT_ADMIN_RESET_PLAIN_PASSWORD = "111111"
+
+
+class UserCRUD(CRUDBase[SysUser]):
+ """用户CRUD操作"""
+
+ def get_by_username(self, db: Session, username: str) -> Optional[SysUser]:
+ """根据用户名获取用户"""
+ return db.query(SysUser).filter(
+ and_(SysUser.username == username, SysUser.is_del == 0)
+ ).first()
+
+ def get_by_phone(self, db: Session, phone: str) -> Optional[SysUser]:
+ """根据手机号获取用户"""
+ return db.query(SysUser).filter(
+ and_(SysUser.phone == phone, SysUser.is_del == 0)
+ ).first()
+
+ def get_by_wx_mini_openid(self, db: Session, openid: str) -> Optional[SysUser]:
+ """根据微信小程序 openid 获取用户"""
+ if not openid:
+ return None
+ return db.query(SysUser).filter(
+ and_(SysUser.wx_mini_openid == openid, SysUser.is_del == 0)
+ ).first()
+
+ def get_multi_by_role(self, db: Session, role: str, skip: int = 0, limit: int = 100) -> List[SysUser]:
+ """根据角色获取用户列表"""
+ return db.query(SysUser).filter(
+ and_(SysUser.role == role, SysUser.is_del == 0)
+ ).offset(skip).limit(limit).all()
+
+ def get_multi_by_region(self, db: Session, region_id: str, skip: int = 0, limit: int = 100) -> List[SysUser]:
+ """根据区域获取用户列表"""
+ return db.query(SysUser).filter(
+ and_(SysUser.region_id == region_id, SysUser.is_del == 0)
+ ).offset(skip).limit(limit).all()
+
+ def get_multi_with_filters(
+ self,
+ db: Session,
+ role: str = None,
+ roles: Optional[List[str]] = None,
+ region_id: str = None,
+ keyword: str = None,
+ is_active: bool = None,
+ skip: int = 0,
+ limit: int = 10
+ ) -> tuple[List[SysUser], int]:
+ """多条件查询用户"""
+ query = db.query(SysUser).filter(SysUser.is_del == 0)
+
+ if role:
+ query = query.filter(SysUser.role == role)
+ elif roles:
+ role_values = [str(x).strip() for x in roles if str(x).strip()]
+ if role_values:
+ query = query.filter(SysUser.role.in_(role_values))
+ if region_id:
+ query = query.filter(SysUser.region_id == region_id)
+ if is_active is not None:
+ query = query.filter(SysUser.is_active == is_active)
+ if keyword:
+ query = query.filter(
+ (SysUser.username.like(f"%{keyword}%")) |
+ (SysUser.real_name.like(f"%{keyword}%")) |
+ (SysUser.phone.like(f"%{keyword}%"))
+ )
+
+ total = query.count()
+ users = query.order_by(SysUser.create_at.desc()).offset(skip).limit(limit).all()
+ return users, total
+
+ def create_with_password(self, db: Session, obj_in: dict, creator: dict = None) -> Tuple[SysUser, Optional[str]]:
+ """创建用户。
+
+ 若请求体包含非空 password(运营端表单),则使用该密码写入 password_hash;
+ 否则生成随机密码,并在返回值中给出明文 initialPassword(仅此种情况返回,供管理员告知用户)。
+ """
+ obj_in = dict(obj_in)
+ raw_pwd = obj_in.pop("password", None)
+ plain: Optional[str] = None
+ if isinstance(raw_pwd, str):
+ plain = raw_pwd.strip()
+ if plain:
+ if len(plain) < 6:
+ raise ValueError("密码至少6位")
+ obj_in["password_hash"] = get_password_hash(plain)
+ initial_reveal: Optional[str] = None
+ else:
+ initial_reveal = generate_random_password()
+ obj_in["password_hash"] = get_password_hash(initial_reveal)
+ obj_in["is_active"] = obj_in.get("is_active", True)
+ user = self.create(db, obj_in, creator)
+ return user, initial_reveal
+
+ def reset_password_to_admin_default(self, db: Session, id: str) -> bool:
+ """将密码重置为服务端固定默认(DEFAULT_ADMIN_RESET_PLAIN_PASSWORD),不由请求传入。"""
+ user = self.get(db, id)
+ if not user:
+ return False
+ plain = DEFAULT_ADMIN_RESET_PLAIN_PASSWORD
+ if len(plain) < 6:
+ raise ValueError("默认重置密码配置无效:至少6位")
+ user.password_hash = get_password_hash(plain)
+ db.commit()
+ return True
+
+ def update_with_history(self, db: Session, id: str, obj_in: dict, updater: dict = None) -> Optional[SysUser]:
+ """更新用户并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return None
+
+ his_data = {
+ "id": original.id,
+ "username": original.username,
+ "password_hash": original.password_hash,
+ "real_name": original.real_name,
+ "phone": original.phone,
+ "avatar_url": getattr(original, "avatar_url", None),
+ "wx_mini_openid": getattr(original, "wx_mini_openid", None),
+ "role": original.role,
+ "region_id": original.region_id,
+ "is_active": original.is_active,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = SysUserHis(**his_data)
+ db.add(his)
+
+ return self.update(db, id, obj_in, updater)
+
+ def soft_delete_with_history(self, db: Session, id: str) -> bool:
+ """删除用户并记录历史"""
+ original = self.get(db, id)
+ if not original:
+ return False
+
+ his_data = {
+ "id": original.id,
+ "username": original.username,
+ "password_hash": original.password_hash,
+ "real_name": original.real_name,
+ "phone": original.phone,
+ "avatar_url": getattr(original, "avatar_url", None),
+ "wx_mini_openid": getattr(original, "wx_mini_openid", None),
+ "role": original.role,
+ "region_id": original.region_id,
+ "is_active": original.is_active,
+ "create_at": original.create_at,
+ "create_by": original.create_by,
+ "create_name": original.create_name,
+ "update_at": original.update_at,
+ "update_by": original.update_by,
+ }
+ his = SysUserHis(**his_data)
+ db.add(his)
+
+ return self.delete(db, id)
+
+
+user_crud = UserCRUD(SysUser)
diff --git a/backend/app/main.py b/backend/app/main.py
new file mode 100644
index 00000000..b59f7b3b
--- /dev/null
+++ b/backend/app/main.py
@@ -0,0 +1,35 @@
+"""FastAPI 骨架入口:无业务路由、无数据库依赖,仅健康检查与文档。"""
+import logging
+
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+from app.core.config import settings
+
+logger = logging.getLogger("zkxw")
+
+app = FastAPI(
+ title=settings.APP_NAME,
+ description="中科兴皖一体化门户后端骨架(业务代码已移除,可按需从版本库恢复)",
+ version="1.0.0",
+ docs_url="/docs",
+ redoc_url="/redoc",
+)
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=settings.BACKEND_CORS_ORIGINS,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+@app.get("/")
+def root():
+ return {"name": settings.APP_NAME, "version": "1.0.0", "docs": "/docs"}
+
+
+@app.get("/health")
+def health_check():
+ return {"status": "healthy"}
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
new file mode 100644
index 00000000..d90e0d5e
--- /dev/null
+++ b/backend/app/models/__init__.py
@@ -0,0 +1,95 @@
+"""数据模型模块"""
+from app.models.base import BaseModel, SoftDeleteModel
+from app.models.models import (
+ SysUser, SysUserHis,
+ SysRegion, SysRegionHis,
+ SysApiRequestLog, SysPasswordHistory,
+ SysApiRequestLogHis, SysPasswordHistoryHis,
+ BasProductCategory, BasProductCategoryHis,
+ BasProductUnit, BasProductUnitHis,
+ BasProduct, BasProductHis,
+ BasChinaRegion,
+ BasAddress, BasAddressHis,
+ BizInquiry, BizInquiryItem,
+ BizQuotation, BizQuotationItem,
+ BizPriceList, BizPriceListItem,
+ BizOrder, BizOrderItem, BizOrderStatusLog, BizOrderAdjust,
+ BizDeliveryTask, BizDeliveryTaskItem,
+ BizSettlement, BizRefund, BizWechatPay, BizAttachment,
+ OrdClientSettlement, OrdClientSettlementHis, OrdClientSettlementItem, OrdClientSettlementItemHis,
+ OrdSupplierSettlement, OrdSupplierSettlementHis, OrdSupplierSettlementItem, OrdSupplierSettlementItemHis,
+ BizInvoice, BizSystemConfig, BizDelivererReview,
+ BizDelivererReviewHis, BizDeliveryWarning, BizDeliveryWarningHis, BizOrderStatusLogHis,
+ UserRole
+)
+from app.models.product_banner import BasProductBanner, BasProductBannerHis
+from app.models.product_detail_media import BasProductDetailMedia, BasProductDetailMediaHis
+from app.models.product_parameter import BasProductParameter, BasProductParameterHis
+from app.models.category_parameter import BasCategoryParameter, BasCategoryParameterHis
+from app.models.product_parameter_value import BasProductParameterValue, BasProductParameterValueHis
+from app.models.product_review import BizProductReview, BizProductReviewHis
+from app.models.product_review_media import BizProductReviewMedia, BizProductReviewMediaHis
+
+# 兼容旧的模型名称别名
+Supplier = BizDelivererReview
+Client = SysUser # 客户复用SysUser,通过role字段区分
+Deliver = SysUser # 配送员复用SysUser,通过role字段区分
+Inquiry = BizInquiry
+Quotation = BizQuotation
+QuotationItem = BizQuotationItem
+PriceList = BizPriceList
+PriceListItem = BizPriceListItem
+Order = BizOrder
+OrderItem = BizOrderItem
+OrderStatus = BizOrderStatusLog
+DeliveryTask = BizDeliveryTask
+DeliveryTaskItem = BizDeliveryTaskItem
+Product = BasProduct
+ProductUnit = BasProductUnit
+Region = SysRegion
+User = SysUser
+ApiRequestLog = SysApiRequestLog
+
+__all__ = [
+ "BaseModel",
+ "SoftDeleteModel",
+ # 系统模块
+ "SysUser", "SysUserHis",
+ "SysRegion", "SysRegionHis",
+ "SysApiRequestLog", "SysPasswordHistory",
+ "SysApiRequestLogHis", "SysPasswordHistoryHis",
+ # 基础模块
+ "BasProductCategory", "BasProductCategoryHis",
+ "BasProductUnit", "BasProductUnitHis",
+ "BasProduct", "BasProductHis",
+ "BasChinaRegion",
+ "BasAddress", "BasAddressHis",
+ # 商品多媒体
+ "BasProductBanner", "BasProductBannerHis",
+ "BasProductDetailMedia", "BasProductDetailMediaHis",
+ # 商品参数
+ "BasProductParameter", "BasProductParameterHis",
+ "BasCategoryParameter", "BasCategoryParameterHis",
+ "BasProductParameterValue", "BasProductParameterValueHis",
+ # 售后评价
+ "BizProductReview", "BizProductReviewHis",
+ "BizProductReviewMedia", "BizProductReviewMediaHis",
+ # 业务模块
+ "BizInquiry", "BizInquiryItem",
+ "BizQuotation", "BizQuotationItem",
+ "BizPriceList", "BizPriceListItem",
+ "BizOrder", "BizOrderItem", "BizOrderStatusLog", "BizOrderAdjust",
+ "BizDeliveryTask", "BizDeliveryTaskItem",
+ "BizSettlement", "BizRefund", "BizWechatPay", "BizAttachment",
+ "OrdClientSettlement", "OrdClientSettlementHis", "OrdClientSettlementItem", "OrdClientSettlementItemHis",
+ "OrdSupplierSettlement", "OrdSupplierSettlementHis", "OrdSupplierSettlementItem", "OrdSupplierSettlementItemHis",
+ "BizInvoice", "BizSystemConfig", "BizDelivererReview",
+ "BizDelivererReviewHis", "BizDeliveryWarning", "BizDeliveryWarningHis", "BizOrderStatusLogHis",
+ "UserRole",
+ # 兼容别名
+ "Supplier", "Inquiry", "Quotation", "QuotationItem",
+ "PriceList", "PriceListItem", "Order", "OrderItem",
+ "OrderStatus", "DeliveryTask", "DeliveryTaskItem",
+ "Product", "ProductUnit", "Region", "User",
+ "ApiRequestLog",
+]
diff --git a/backend/app/models/base.py b/backend/app/models/base.py
new file mode 100644
index 00000000..97e56b76
--- /dev/null
+++ b/backend/app/models/base.py
@@ -0,0 +1,63 @@
+"""基础模型类"""
+from datetime import datetime
+from typing import Optional, Any
+from sqlalchemy import Column, String, DateTime, Integer, func
+from sqlalchemy.ext.declarative import declared_attr
+
+from app.core.database import Base
+
+
+class BaseModel(Base):
+ """所有模型的基类,包含通用字段"""
+ __abstract__ = True
+
+ @declared_attr
+ def __tablename__(cls) -> str:
+ """自动生成表名"""
+ import re
+ name = cls.__name__
+ # 驼峰转下划线
+ return re.sub(r'(? dict:
+ """转换为字典"""
+ if exclude is None:
+ exclude = []
+ result = {}
+ for column in self.__table__.columns:
+ if column.name not in exclude:
+ value = getattr(self, column.name)
+ if isinstance(value, datetime):
+ result[column.name] = value.strftime("%Y-%m-%d %H:%M:%S")
+ else:
+ result[column.name] = value
+ return result
+
+
+class SoftDeleteModel(BaseModel):
+ """支持软删除的模型基类"""
+ __abstract__ = True
+
+ is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是")
+
+ @classmethod
+ def get_active(cls, db, **kwargs):
+ """获取未删除的记录"""
+ return db.query(cls).filter(cls.is_del == 0, **kwargs)
+
+ def soft_delete(self, db):
+ """软删除"""
+ self.is_del = 1
+ db.commit()
+
+ def restore(self, db):
+ """恢复删除"""
+ self.is_del = 0
+ db.commit()
diff --git a/backend/app/models/category_parameter.py b/backend/app/models/category_parameter.py
new file mode 100644
index 00000000..e5d85a94
--- /dev/null
+++ b/backend/app/models/category_parameter.py
@@ -0,0 +1,42 @@
+"""分类参数关联模型"""
+from datetime import datetime
+
+from sqlalchemy import Column, String, DateTime, Integer
+
+from app.core.database import Base
+
+
+class BasCategoryParameter(Base):
+ """商品分类-参数关联表"""
+ __tablename__ = "bas_category_parameter"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ category_id = Column(String(32), nullable=False, comment="分类ID")
+ parameter_id = Column(String(32), nullable=False, comment="参数ID")
+ is_required = Column(Integer, default=0, comment="该分类下是否必填:0-否, 1-是")
+ default_value = Column(String(500), nullable=True, comment="默认值")
+ sort_order = Column(Integer, default=0, comment="排序")
+ is_del = Column(Integer, default=0, comment="是否删除:0-否, 1-是")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BasCategoryParameterHis(Base):
+ """商品分类-参数关联表历史表"""
+ __tablename__ = "bas_category_parameter_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ category_id = Column(String(32), nullable=True, comment="分类ID")
+ parameter_id = Column(String(32), nullable=True, comment="参数ID")
+ is_required = Column(Integer, default=0, comment="是否必填")
+ default_value = Column(String(500), nullable=True, comment="默认值")
+ sort_order = Column(Integer, default=0, comment="排序")
+ create_at = Column(DateTime, nullable=True, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
diff --git a/backend/app/models/models.py b/backend/app/models/models.py
new file mode 100644
index 00000000..580a36d4
--- /dev/null
+++ b/backend/app/models/models.py
@@ -0,0 +1,1550 @@
+"""数据模型"""
+from datetime import datetime
+
+from sqlalchemy import Column, String, DateTime, Integer, Boolean, Text, Numeric, PrimaryKeyConstraint
+from sqlalchemy.orm import relationship
+
+from app.core.database import Base
+import enum
+
+
+class UserRole(str, enum.Enum):
+ """用户角色"""
+ ADMIN = "admin" # 运营人员
+ SUPPLIER = "supplier" # 供应商
+ CLIENT = "client" # 客户
+ DELIVER = "deliver" # 配送人员
+
+
+# =========================================
+# 系统模块
+# =========================================
+
+class SysUser(Base):
+ """用户表"""
+ __tablename__ = "sys_user"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ username = Column(String(50), unique=True, nullable=False, comment="用户名")
+ password_hash = Column(String(255), nullable=False, comment="密码哈希")
+ real_name = Column(String(100), nullable=True, comment="真实姓名")
+ phone = Column(String(20), nullable=True, comment="联系电话")
+ avatar_url = Column(String(512), nullable=True, comment="头像URL")
+ wx_mini_openid = Column(String(64), nullable=True, unique=True, comment="微信小程序openid")
+ role = Column(String(20), nullable=False, comment="角色")
+ region_id = Column(String(32), nullable=True, comment="所属区域ID")
+ is_active = Column(Boolean, default=True, comment="是否激活")
+ is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class SysUserHis(Base):
+ """用户历史表"""
+ __tablename__ = "sys_user_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ username = Column(String(50), nullable=False, comment="用户名")
+ password_hash = Column(String(255), nullable=False, comment="密码哈希")
+ real_name = Column(String(100), nullable=True, comment="真实姓名")
+ phone = Column(String(20), nullable=True, comment="联系电话")
+ avatar_url = Column(String(512), nullable=True, comment="头像URL")
+ wx_mini_openid = Column(String(64), nullable=True, comment="微信小程序openid")
+ role = Column(String(20), nullable=False, comment="角色")
+ region_id = Column(String(32), nullable=True, comment="所属区域ID")
+ is_active = Column(Boolean, default=True, comment="是否激活")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class SysRegion(Base):
+ """区域表"""
+ __tablename__ = "sys_region"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ name = Column(String(100), nullable=False, comment="区域名称")
+ code = Column(String(50), unique=True, nullable=False, comment="区域编码")
+ description = Column(String(500), nullable=True, comment="区域描述")
+ sort_order = Column(Integer, default=0, comment="排序")
+ expected_delivery_minutes = Column(Integer, nullable=True, comment="预计配送时长(分钟)")
+ delivery_warn_advance_minutes = Column(
+ Integer, nullable=True, comment="计划到货前未送达时预警提前时间(分钟)"
+ )
+ is_active = Column(Boolean, default=True, comment="是否启用")
+ is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class SysRegionHis(Base):
+ """区域历史表"""
+ __tablename__ = "sys_region_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ name = Column(String(100), nullable=False, comment="区域名称")
+ code = Column(String(50), nullable=False, comment="区域编码")
+ description = Column(String(500), nullable=True, comment="区域描述")
+ sort_order = Column(Integer, default=0, comment="排序")
+ expected_delivery_minutes = Column(Integer, nullable=True, comment="预计配送时长(分钟)")
+ delivery_warn_advance_minutes = Column(
+ Integer, nullable=True, comment="计划到货前未送达时预警提前时间(分钟)"
+ )
+ is_active = Column(Boolean, default=True, comment="是否启用")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BasChinaRegion(Base):
+ """中国行政区划(GB/T 2260),见 docs/sql/bas_china_region.sql"""
+
+ __tablename__ = "bas_china_region"
+
+ id = Column(String(32), primary_key=True, comment="主键,与 region_code 一致")
+ parent_id = Column(String(32), nullable=True, comment="父级 region_code,省级为 NULL")
+ region_code = Column(String(6), nullable=False, comment="六位行政区划码")
+ region_name = Column(String(100), nullable=False, comment="区划名称")
+ level = Column(Integer, nullable=False, comment="层级:1省 2市 3区县")
+ sort_order = Column(Integer, nullable=False, default=0, comment="排序")
+ is_del = Column(Integer, default=0, nullable=False, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class SysApiRequestLog(Base):
+ """API 请求日志(统一记录请求/响应出入参)"""
+ __tablename__ = "sys_api_request_log"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ http_method = Column(String(10), nullable=False, comment="HTTP 方法")
+ path = Column(String(500), nullable=False, comment="请求路径")
+ query_string = Column(String(2000), nullable=True, comment="Query 字符串")
+ request_body = Column(Text, nullable=True, comment="请求体(截断)")
+ response_body = Column(Text, nullable=True, comment="响应体(截断)")
+ status_code = Column(Integer, nullable=False, comment="HTTP 状态码")
+ duration_ms = Column(Integer, nullable=False, comment="耗时(ms)")
+ user_id = Column(String(32), nullable=True, comment="用户ID(JWT 解析)")
+ username = Column(String(100), nullable=True, comment="用户名")
+ role = Column(String(20), nullable=True, comment="角色")
+ ip_address = Column(String(50), nullable=True, comment="IP地址")
+ user_agent = Column(String(500), nullable=True, comment="User-Agent")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class SysMenu(Base):
+ """菜单表"""
+ __tablename__ = "sys_menu"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ name = Column(String(100), nullable=False, comment="菜单名称")
+ code = Column(String(100), nullable=False, comment="菜单编码(唯一标识)")
+ path = Column(String(500), nullable=False, comment="路由路径")
+ component = Column(String(500), nullable=True, comment="组件路径")
+ icon = Column(String(100), nullable=True, comment="菜单图标")
+ parent_id = Column(String(32), nullable=True, comment="父菜单ID")
+ sort_order = Column(Integer, default=0, comment="排序(数字越小越靠前)")
+ redirect = Column(String(500), nullable=True, comment="重定向路径")
+ menu_type = Column(String(20), default="menu", comment="菜单类型: menu-菜单, button-按钮")
+ is_visible = Column(Integer, default=1, comment="是否显示: 0-隐藏, 1-显示")
+ is_cache = Column(Integer, default=0, comment="是否缓存: 0-不缓存, 1-缓存")
+ is_active = Column(Integer, default=1, comment="是否启用: 0-禁用, 1-启用")
+ is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是")
+ remark = Column(String(500), nullable=True, comment="备注")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class SysUserRegionScope(Base):
+ """用户区域权限表"""
+ __tablename__ = "sys_user_region_scope"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ user_id = Column(String(32), nullable=False, comment="用户ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class SysUserRegionScopeHis(Base):
+ """用户区域权限历史表"""
+ __tablename__ = "sys_user_region_scope_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ user_id = Column(String(32), nullable=False, comment="用户ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class SysUserMenuScope(Base):
+ """用户菜单权限表(按菜单 code 授权)"""
+ __tablename__ = "sys_user_menu_scope"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ user_id = Column(String(32), nullable=False, comment="用户ID")
+ menu_code = Column(String(100), nullable=False, comment="菜单编码")
+ is_del = Column(Integer, default=0, comment="是否删除: 0-否, 1-是")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class SysUserMenuScopeHis(Base):
+ """用户菜单权限历史表"""
+ __tablename__ = "sys_user_menu_scope_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ user_id = Column(String(32), nullable=False, comment="用户ID")
+ menu_code = Column(String(100), nullable=False, comment="菜单编码")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class SysPasswordHistory(Base):
+ """密码修改历史表"""
+ __tablename__ = "sys_password_history"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ user_id = Column(String(32), nullable=False, comment="用户ID")
+ operation_type = Column(String(20), nullable=False, comment="操作类型")
+ operator_id = Column(String(32), nullable=True, comment="操作人ID")
+ operator_name = Column(String(100), nullable=True, comment="操作人姓名")
+ ip_address = Column(String(50), nullable=True, comment="IP地址")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+# =========================================
+# 基础模块
+# =========================================
+
+class BasProductCategory(Base):
+ """商品分类表"""
+ __tablename__ = "bas_product_category"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ name = Column(String(100), nullable=False, comment="分类名称")
+ image_url = Column(String(500), nullable=True, comment="图片URL")
+ parent_id = Column(String(32), nullable=True, comment="父分类ID")
+ sort_order = Column(Integer, default=0, comment="排序")
+ price_coefficient = Column(
+ Numeric(6, 2), default=0, nullable=False, comment="价格系数(百分比点数,5表示5%)"
+ )
+ is_active = Column(Boolean, default=True, comment="是否启用")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BasProductCategoryHis(Base):
+ """商品分类历史表"""
+ __tablename__ = "bas_product_category_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ name = Column(String(100), nullable=False, comment="分类名称")
+ image_url = Column(String(500), nullable=True, comment="图片URL")
+ parent_id = Column(String(32), nullable=True, comment="父分类ID")
+ sort_order = Column(Integer, default=0, comment="排序")
+ price_coefficient = Column(
+ Numeric(6, 2), default=0, nullable=False, comment="价格系数(百分比点数,5表示5%)"
+ )
+ is_active = Column(Boolean, default=True, comment="是否启用")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BasProductUnit(Base):
+ """商品单位码表"""
+ __tablename__ = "bas_product_unit"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ name = Column(String(50), nullable=False, comment="单位名称")
+ code = Column(String(20), unique=True, nullable=False, comment="单位编码")
+ is_active = Column(Boolean, default=True, comment="是否启用")
+ sort_order = Column(Integer, default=0, comment="排序字段,数值越小越靠前")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BasProductUnitHis(Base):
+ """商品单位历史表"""
+ __tablename__ = "bas_product_unit_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ name = Column(String(50), nullable=False, comment="单位名称")
+ code = Column(String(20), nullable=False, comment="单位编码")
+ is_active = Column(Boolean, default=True, comment="是否启用")
+ sort_order = Column(Integer, default=0, comment="排序字段,数值越小越靠前")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BasProduct(Base):
+ """商品表"""
+ __tablename__ = "bas_product"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ name = Column(String(200), nullable=False, comment="商品名称")
+ sku_code = Column(String(50), nullable=True, unique=True, comment="商品SKU编码")
+ category_id = Column(String(32), nullable=True, comment="分类ID")
+ unit_id = Column(String(32), nullable=True, comment="单位ID")
+ measure_value = Column(Numeric(12, 3), nullable=True, comment="计量数量(与单位组合表示售卖规格,如500克、1个)")
+ description = Column(String(1000), nullable=True, comment="商品描述")
+ detail_content = Column(Text, nullable=True, comment="商品详情富文本内容")
+ image_url = Column(String(500), nullable=True, comment="图片URL")
+ price_coefficient = Column(
+ Numeric(6, 2), default=0, nullable=False, comment="调价系数(百分比点数,5表示5%)"
+ )
+ is_active = Column(Boolean, default=True, comment="是否启用")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+ category = relationship(
+ "BasProductCategory",
+ foreign_keys=[category_id],
+ primaryjoin="BasProduct.category_id==BasProductCategory.id",
+ lazy="select",
+ )
+ unit = relationship(
+ "BasProductUnit",
+ foreign_keys=[unit_id],
+ primaryjoin="BasProduct.unit_id==BasProductUnit.id",
+ lazy="select",
+ )
+
+
+class BasProductHis(Base):
+ """商品历史表"""
+ __tablename__ = "bas_product_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ name = Column(String(200), nullable=False, comment="商品名称")
+ sku_code = Column(String(50), nullable=True, comment="商品SKU编码")
+ category_id = Column(String(32), nullable=True, comment="分类ID")
+ unit_id = Column(String(32), nullable=True, comment="单位ID")
+ measure_value = Column(Numeric(12, 3), nullable=True, comment="计量数量(与单位组合表示售卖规格,如500克、1个)")
+ description = Column(String(1000), nullable=True, comment="商品描述")
+ detail_content = Column(Text, nullable=True, comment="商品详情富文本内容")
+ image_url = Column(String(500), nullable=True, comment="图片URL")
+ price_coefficient = Column(
+ Numeric(6, 2), default=0, nullable=False, comment="调价系数(百分比点数,5表示5%)"
+ )
+ is_active = Column(Boolean, default=True, comment="是否启用")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BasAddress(Base):
+ """地址表"""
+ __tablename__ = "bas_address"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ user_id = Column(String(32), nullable=False, comment="用户ID")
+ address_type = Column(String(20), nullable=False, comment="地址类型")
+ contact_person = Column(String(100), nullable=False, comment="联系人")
+ contact_phone = Column(String(20), nullable=False, comment="联系电话")
+ province = Column(String(50), nullable=True, comment="省份")
+ city = Column(String(50), nullable=True, comment="城市")
+ district = Column(String(50), nullable=True, comment="区县")
+ detail_address = Column(String(500), nullable=False, comment="详细地址")
+ is_default = Column(Boolean, default=False, comment="是否默认地址")
+ latitude = Column(String(20), nullable=True, comment="纬度")
+ longitude = Column(String(20), nullable=True, comment="经度")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BasAddressHis(Base):
+ """地址历史表"""
+ __tablename__ = "bas_address_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ user_id = Column(String(32), nullable=False, comment="用户ID")
+ address_type = Column(String(20), nullable=False, comment="地址类型")
+ contact_person = Column(String(100), nullable=False, comment="联系人")
+ contact_phone = Column(String(20), nullable=False, comment="联系电话")
+ province = Column(String(50), nullable=True, comment="省份")
+ city = Column(String(50), nullable=True, comment="城市")
+ district = Column(String(50), nullable=True, comment="区县")
+ detail_address = Column(String(500), nullable=False, comment="详细地址")
+ is_default = Column(Boolean, default=False, comment="是否默认地址")
+ latitude = Column(String(20), nullable=True, comment="纬度")
+ longitude = Column(String(20), nullable=True, comment="经度")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+# =========================================
+# 业务模块
+# =========================================
+
+class BizInquiry(Base):
+ """询价单表"""
+ __tablename__ = "biz_inquiry"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ title = Column(String(200), nullable=False, comment="询价单标题")
+ status = Column(String(20), default="draft", comment="状态")
+ publish_time = Column(DateTime, nullable=True, comment="发布时间")
+ close_time = Column(DateTime, nullable=True, comment="关闭时间")
+ remark = Column(String(1000), nullable=True, comment="备注")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizInquiryItem(Base):
+ """询价单项表"""
+ __tablename__ = "biz_inquiry_item"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ inquiry_id = Column(String(32), nullable=False, comment="询价单ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizInquiryHis(Base):
+ """询价单历史表"""
+ __tablename__ = "biz_inquiry_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ title = Column(String(200), nullable=False, comment="询价单标题")
+ status = Column(String(20), default="draft", comment="状态")
+ publish_time = Column(DateTime, nullable=True, comment="发布时间")
+ close_time = Column(DateTime, nullable=True, comment="关闭时间")
+ remark = Column(String(1000), nullable=True, comment="备注")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizInquiryItemHis(Base):
+ """询价单项历史表"""
+ __tablename__ = "biz_inquiry_item_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ inquiry_id = Column(String(32), nullable=False, comment="询价单ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizQuotation(Base):
+ """报价单表"""
+ __tablename__ = "biz_quotation"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ inquiry_id = Column(String(32), nullable=False, comment="询价单ID")
+ supplier_id = Column(String(32), nullable=False, comment="供应商ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ # 报价无需审核:仅使用 submitted / selected
+ status = Column(String(20), default="submitted", comment="状态")
+ total_amount = Column(Numeric(12, 2), default=0, comment="报价总金额")
+ submit_time = Column(DateTime, nullable=True, comment="提交时间")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizQuotationItem(Base):
+ """报价单项表"""
+ __tablename__ = "biz_quotation_item"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ quotation_id = Column(String(32), nullable=False, comment="报价单ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ unit_id = Column(String(32), nullable=False, comment="单位ID")
+ price = Column(Numeric(10, 2), nullable=False, comment="单价")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizQuotationHis(Base):
+ """报价单历史表"""
+ __tablename__ = "biz_quotation_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ inquiry_id = Column(String(32), nullable=False, comment="询价单ID")
+ supplier_id = Column(String(32), nullable=False, comment="供应商ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ status = Column(String(20), default="submitted", comment="状态")
+ total_amount = Column(Numeric(12, 2), default=0, comment="报价总金额")
+ submit_time = Column(DateTime, nullable=True, comment="提交时间")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizQuotationItemHis(Base):
+ """报价单项历史表"""
+ __tablename__ = "biz_quotation_item_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ quotation_id = Column(String(32), nullable=False, comment="报价单ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ unit_id = Column(String(32), nullable=False, comment="单位ID")
+ price = Column(Numeric(10, 2), nullable=False, comment="单价")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizPriceList(Base):
+ """价格清单表"""
+ __tablename__ = "biz_price_list"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ supplier_id = Column(String(32), nullable=False, comment="选用供应商ID")
+ inquiry_id = Column(String(32), nullable=True, comment="来源询价单ID")
+ quotation_id = Column(String(32), nullable=True, comment="来源报价单ID")
+ source_type = Column(String(20), nullable=True, comment="来源类型")
+ version = Column(Integer, default=1, comment="版本号")
+ status = Column(String(20), default="draft", comment="状态")
+ effective_time = Column(DateTime, nullable=True, comment="生效时间")
+ published_by = Column(String(32), nullable=True, comment="发布人ID")
+ published_time = Column(DateTime, nullable=True, comment="发布时间")
+ remark = Column(String(500), nullable=True, comment="备注")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizPriceListHis(Base):
+ """价格清单历史表"""
+ __tablename__ = "biz_price_list_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ supplier_id = Column(String(32), nullable=False, comment="选用供应商ID")
+ inquiry_id = Column(String(32), nullable=True, comment="来源询价单ID")
+ quotation_id = Column(String(32), nullable=True, comment="来源报价单ID")
+ source_type = Column(String(20), nullable=True, comment="来源类型")
+ version = Column(Integer, default=1, comment="版本号")
+ status = Column(String(20), default="draft", comment="状态")
+ effective_time = Column(DateTime, nullable=True, comment="生效时间")
+ published_by = Column(String(32), nullable=True, comment="发布人ID")
+ published_time = Column(DateTime, nullable=True, comment="发布时间")
+ remark = Column(String(500), nullable=True, comment="备注")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizPriceListItem(Base):
+ """价格清单项表"""
+ __tablename__ = "biz_price_list_item"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ price_list_id = Column(String(32), nullable=False, comment="价格清单ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ unit_id = Column(String(32), nullable=False, comment="单位ID")
+ price = Column(Numeric(10, 2), nullable=False, comment="单价")
+ pricing_coefficient = Column(Numeric(8, 2), nullable=True, comment="本次发布使用的调价系数(%)")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizOrder(Base):
+ """订单主表"""
+ __tablename__ = "biz_order"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ order_no = Column(String(50), unique=True, nullable=False, comment="订单编号")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ price_list_id = Column(String(32), nullable=False, comment="价格清单ID")
+ price_version = Column(Integer, nullable=False, comment="价格版本号")
+ status = Column(String(30), nullable=False, default="pending_payment", comment="订单状态")
+ # 三字段状态(已统一为新模型)
+ order_status = Column(String(20), nullable=False, default="pending_payment", comment="订单主状态: pending_payment/pending_confirm/completed/cancelled")
+ delivery_status = Column(String(20), nullable=False, default="pending_dispatch", comment="配送状态: pending_dispatch/delivering/delivered")
+ pay_status = Column(String(20), nullable=False, default="pending_payment", comment="支付状态: pending_payment/paid/refunding/refunded")
+ cancel_status = Column(String(20), nullable=False, default="none", comment="取消审核状态: none/pending_review/approved/rejected")
+ payment_method = Column(String(20), nullable=True, comment="支付渠道: online=线上(微信等), offline=线下结算(与 pay_status 独立)")
+ client_settlement_status = Column(String(20), nullable=False, default="pending", comment="客户结算状态: pending/settled")
+ supplier_settlement_status = Column(String(20), nullable=False, default="pending", comment="供应商结算状态: pending/settled")
+ client_settlement_at = Column(DateTime, nullable=True, comment="客户结算时间")
+ supplier_settlement_at = Column(DateTime, nullable=True, comment="供应商结算时间")
+ total_amount = Column(Numeric(12, 2), default=0, comment="订单总金额")
+ paid_amount = Column(Numeric(12, 2), default=0, comment="已付金额")
+ receivable_amount = Column(Numeric(12, 2), default=0, comment="应收金额")
+ refund_amount = Column(Numeric(12, 2), default=0, comment="退款金额")
+ contact_person = Column(String(100), nullable=True, comment="联系人")
+ contact_phone = Column(String(20), nullable=True, comment="联系电话")
+ delivery_address = Column(String(500), nullable=True, comment="配送地址")
+ cancel_reason = Column(String(500), nullable=True, comment="取消原因")
+ cancel_review_time = Column(DateTime, nullable=True, comment="取消审核时间")
+ cancel_reviewed_by = Column(String(32), nullable=True, comment="取消审核人ID")
+ cancel_review_note = Column(String(500), nullable=True, comment="取消审核备注")
+ completed_time = Column(DateTime, nullable=True, comment="完结时间")
+ completed_by = Column(String(32), nullable=True, comment="完结人ID")
+ remark = Column(Text, nullable=True, comment="订单备注")
+ payment_time = Column(DateTime, nullable=True, comment="支付时间")
+ delivery_assign_time = Column(DateTime, nullable=True, comment="配送分配时间")
+ delivered_time = Column(DateTime, nullable=True, comment="配送完成时间(待客户确认收货起算点)")
+ lock_operator_id = Column(String(32), nullable=True, comment="锁定操作人ID")
+ lock_time = Column(DateTime, nullable=True, comment="锁定时间")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizOrderItem(Base):
+ """订单项表"""
+ __tablename__ = "biz_order_item"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ unit_id = Column(String(32), nullable=False, comment="单位ID")
+ quantity = Column(Numeric(12, 4), default=0, comment="数量")
+ unit_price = Column(Numeric(10, 2), nullable=False, comment="单价")
+ amount = Column(Numeric(12, 2), default=0, comment="金额")
+ delivered_quantity = Column(Numeric(12, 4), default=0, comment="已配送数量")
+ status = Column(String(20), default="normal", comment="项状态")
+ note = Column(String(500), nullable=True, comment="备注")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizOrderHis(Base):
+ """订单主表历史表"""
+ __tablename__ = "biz_order_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ order_no = Column(String(50), nullable=False, comment="订单编号")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ price_list_id = Column(String(32), nullable=False, comment="价格清单ID")
+ price_version = Column(Integer, nullable=False, comment="价格版本号")
+ status = Column(String(30), nullable=False, default="pending_payment", comment="订单状态")
+ # 三字段状态(历史表同步保留新模型)
+ order_status = Column(String(20), nullable=False, default="pending_payment", comment="订单主状态: pending_payment/pending_confirm/completed/cancelled")
+ delivery_status = Column(String(20), nullable=False, default="pending_dispatch", comment="配送状态: pending_dispatch/delivering/delivered")
+ pay_status = Column(String(20), nullable=False, default="pending_payment", comment="支付状态: pending_payment/paid/refunding/refunded")
+ cancel_status = Column(String(20), nullable=False, default="none", comment="取消审核状态: none/pending_review/approved/rejected")
+ payment_method = Column(String(20), nullable=True, comment="支付渠道: online=线上(微信等), offline=线下结算(与 pay_status 独立)")
+ client_settlement_status = Column(String(20), nullable=False, default="pending", comment="客户结算状态: pending/settled")
+ supplier_settlement_status = Column(String(20), nullable=False, default="pending", comment="供应商结算状态: pending/settled")
+ client_settlement_at = Column(DateTime, nullable=True, comment="客户结算时间")
+ supplier_settlement_at = Column(DateTime, nullable=True, comment="供应商结算时间")
+ total_amount = Column(Numeric(12, 2), default=0, comment="订单总金额")
+ paid_amount = Column(Numeric(12, 2), default=0, comment="已付金额")
+ receivable_amount = Column(Numeric(12, 2), default=0, comment="应收金额")
+ refund_amount = Column(Numeric(12, 2), default=0, comment="退款金额")
+ contact_person = Column(String(100), nullable=True, comment="联系人")
+ contact_phone = Column(String(20), nullable=True, comment="联系电话")
+ delivery_address = Column(String(500), nullable=True, comment="配送地址")
+ cancel_reason = Column(String(500), nullable=True, comment="取消原因")
+ cancel_review_time = Column(DateTime, nullable=True, comment="取消审核时间")
+ cancel_reviewed_by = Column(String(32), nullable=True, comment="取消审核人ID")
+ cancel_review_note = Column(String(500), nullable=True, comment="取消审核备注")
+ completed_time = Column(DateTime, nullable=True, comment="完结时间")
+ completed_by = Column(String(32), nullable=True, comment="完结人ID")
+ remark = Column(Text, nullable=True, comment="订单备注")
+ payment_time = Column(DateTime, nullable=True, comment="支付时间")
+ delivery_assign_time = Column(DateTime, nullable=True, comment="配送分配时间")
+ delivered_time = Column(DateTime, nullable=True, comment="配送完成时间(待客户确认收货起算点)")
+ lock_operator_id = Column(String(32), nullable=True, comment="锁定操作人ID")
+ lock_time = Column(DateTime, nullable=True, comment="锁定时间")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizOrderStatusLog(Base):
+ """订单状态变更日志表"""
+ __tablename__ = "biz_order_status_log"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ from_status = Column(String(30), nullable=True, comment="变更前状态")
+ to_status = Column(String(30), nullable=False, comment="变更后状态")
+ operator_id = Column(String(32), nullable=False, comment="操作人ID")
+ operator_role = Column(String(20), nullable=False, comment="操作人角色")
+ reason = Column(String(500), nullable=True, comment="变更原因")
+ note = Column(String(1000), nullable=True, comment="备注")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizDeliveryTask(Base):
+ """配送任务表"""
+ __tablename__ = "biz_delivery_task"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ task_no = Column(String(50), unique=True, nullable=False, comment="配送单编号")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ supplier_id = Column(String(32), nullable=False, comment="供应商ID")
+ deliver_id = Column(String(32), nullable=True, comment="配送员ID")
+ status = Column(String(20), default="pending", comment="状态")
+ deliverer_status = Column(String(20), default="pending_pickup", comment="配送员状态: pending_pickup/delivering/delivered")
+ supplier_prepare_status = Column(String(20), default="pending_prepare", comment="供应商备货状态: pending_prepare/prepared/picked_up")
+ pickup_time = Column(DateTime, nullable=True, comment="取货时间")
+ pickup_remark = Column(String(500), nullable=True, comment="取货备注")
+ pickup_image_url = Column(String(500), nullable=True, comment="取货凭证图片")
+ delivery_time = Column(DateTime, nullable=True, comment="送达时间")
+ delivery_remark = Column(String(500), nullable=True, comment="送达备注")
+ delivery_image_url = Column(String(500), nullable=True, comment="送达凭证图片")
+ expire_warning_sent = Column(Boolean, default=False, comment="是否已发送过期预警")
+ effective_time = Column(DateTime, nullable=True, comment="生效时间")
+ dispatch_remark = Column(String(500), nullable=True, comment="运营分配配送备注")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizDeliveryTaskItem(Base):
+ """配送任务明细表"""
+ __tablename__ = "biz_delivery_task_item"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ task_id = Column(String(32), nullable=False, comment="配送任务ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ order_item_id = Column(String(32), nullable=False, comment="订单项ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ delivery_address = Column(String(500), nullable=False, comment="配送地址")
+ contact_person = Column(String(100), nullable=False, comment="联系人")
+ contact_phone = Column(String(20), nullable=False, comment="联系电话")
+ quantity = Column(Numeric(12, 4), default=0, comment="配送数量")
+ delivered_quantity = Column(Numeric(12, 4), default=0, comment="已送达数量")
+ status = Column(String(20), default="pending", comment="状态")
+ pickup_time = Column(DateTime, nullable=True, comment="取货时间")
+ delivery_time = Column(DateTime, nullable=True, comment="送达时间")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizDeliveryTaskHis(Base):
+ """配送任务历史表"""
+ __tablename__ = "biz_delivery_task_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ task_no = Column(String(50), nullable=False, comment="配送单编号")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ supplier_id = Column(String(32), nullable=False, comment="供应商ID")
+ deliver_id = Column(String(32), nullable=True, comment="配送员ID")
+ status = Column(String(20), default="pending", comment="状态")
+ deliverer_status = Column(String(20), default="pending_pickup", comment="配送员状态: pending_pickup/delivering/delivered")
+ supplier_prepare_status = Column(String(20), default="pending_prepare", comment="供应商备货状态: pending_prepare/prepared/picked_up")
+ pickup_time = Column(DateTime, nullable=True, comment="取货时间")
+ pickup_remark = Column(String(500), nullable=True, comment="取货备注")
+ pickup_image_url = Column(String(500), nullable=True, comment="取货凭证图片")
+ delivery_time = Column(DateTime, nullable=True, comment="送达时间")
+ delivery_remark = Column(String(500), nullable=True, comment="送达备注")
+ delivery_image_url = Column(String(500), nullable=True, comment="送达凭证图片")
+ expire_warning_sent = Column(Boolean, default=False, comment="是否已发送过期预警")
+ effective_time = Column(DateTime, nullable=True, comment="生效时间")
+ dispatch_remark = Column(String(500), nullable=True, comment="运营分配配送备注")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizSettlement(Base):
+ """结算记录表"""
+ __tablename__ = "biz_settlement"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ amount = Column(Numeric(12, 2), nullable=False, comment="结算金额")
+ settlement_method = Column(String(20), nullable=False, comment="结算方式")
+ settlement_time = Column(DateTime, nullable=True, comment="结算时间")
+ settlement_file_url = Column(String(500), nullable=True, comment="结算凭证文件URL")
+ settlement_note = Column(String(500), nullable=True, comment="结算备注")
+ status = Column(String(20), default="pending", comment="状态")
+ reviewed_by = Column(String(32), nullable=True, comment="审核人ID")
+ reviewed_time = Column(DateTime, nullable=True, comment="审核时间")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class OrdClientSettlement(Base):
+ """客户结算主表"""
+ __tablename__ = "ord_client_settlement"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ settlement_no = Column(String(50), nullable=False, unique=True, comment="结算单号")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ client_name = Column(String(100), nullable=True, comment="客户名称快照")
+ order_count = Column(Integer, nullable=False, default=0, comment="订单数量")
+ total_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="结算总金额")
+ voucher_urls = Column(Text, nullable=True, comment="结算凭证URL(JSON数组)")
+ remark = Column(String(500), nullable=True, comment="结算备注")
+ settlement_at = Column(DateTime, nullable=False, default=datetime.now, comment="结算时间")
+ status = Column(String(20), nullable=False, default="done", comment="结算单状态")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class OrdClientSettlementHis(Base):
+ """客户结算主表历史"""
+ __tablename__ = "ord_client_settlement_his"
+ __table_args__ = (PrimaryKeyConstraint("id", "his_create_at", name="pk_ord_client_settlement_his"),)
+
+ id = Column(String(32), nullable=False, comment="主键ID")
+ settlement_no = Column(String(50), nullable=False, comment="结算单号")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ client_name = Column(String(100), nullable=True, comment="客户名称快照")
+ order_count = Column(Integer, nullable=False, default=0, comment="订单数量")
+ total_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="结算总金额")
+ voucher_urls = Column(Text, nullable=True, comment="结算凭证URL(JSON数组)")
+ remark = Column(String(500), nullable=True, comment="结算备注")
+ settlement_at = Column(DateTime, nullable=False, default=datetime.now, comment="结算时间")
+ status = Column(String(20), nullable=False, default="done", comment="结算单状态")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class OrdClientSettlementItem(Base):
+ """客户结算明细表"""
+ __tablename__ = "ord_client_settlement_item"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ settlement_id = Column(String(32), nullable=False, comment="结算主表ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ order_no = Column(String(50), nullable=False, comment="订单号")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ order_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="订单结算金额")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class OrdClientSettlementItemHis(Base):
+ """客户结算明细历史表"""
+ __tablename__ = "ord_client_settlement_item_his"
+ __table_args__ = (PrimaryKeyConstraint("id", "his_create_at", name="pk_ord_client_settlement_item_his"),)
+
+ id = Column(String(32), nullable=False, comment="主键ID")
+ settlement_id = Column(String(32), nullable=False, comment="结算主表ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ order_no = Column(String(50), nullable=False, comment="订单号")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ order_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="订单结算金额")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class OrdSupplierSettlement(Base):
+ """供应商结算主表"""
+ __tablename__ = "ord_supplier_settlement"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ settlement_no = Column(String(50), nullable=False, unique=True, comment="结算单号")
+ supplier_id = Column(String(32), nullable=False, comment="供应商ID")
+ supplier_name = Column(String(100), nullable=True, comment="供应商名称快照")
+ order_count = Column(Integer, nullable=False, default=0, comment="订单数量")
+ total_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="结算总金额")
+ voucher_urls = Column(Text, nullable=True, comment="结算凭证URL(JSON数组)")
+ remark = Column(String(500), nullable=True, comment="结算备注")
+ settlement_at = Column(DateTime, nullable=False, default=datetime.now, comment="结算时间")
+ status = Column(String(20), nullable=False, default="done", comment="结算单状态")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class OrdSupplierSettlementHis(Base):
+ """供应商结算主表历史"""
+ __tablename__ = "ord_supplier_settlement_his"
+ __table_args__ = (PrimaryKeyConstraint("id", "his_create_at", name="pk_ord_supplier_settlement_his"),)
+
+ id = Column(String(32), nullable=False, comment="主键ID")
+ settlement_no = Column(String(50), nullable=False, comment="结算单号")
+ supplier_id = Column(String(32), nullable=False, comment="供应商ID")
+ supplier_name = Column(String(100), nullable=True, comment="供应商名称快照")
+ order_count = Column(Integer, nullable=False, default=0, comment="订单数量")
+ total_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="结算总金额")
+ voucher_urls = Column(Text, nullable=True, comment="结算凭证URL(JSON数组)")
+ remark = Column(String(500), nullable=True, comment="结算备注")
+ settlement_at = Column(DateTime, nullable=False, default=datetime.now, comment="结算时间")
+ status = Column(String(20), nullable=False, default="done", comment="结算单状态")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class OrdSupplierSettlementItem(Base):
+ """供应商结算明细表"""
+ __tablename__ = "ord_supplier_settlement_item"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ settlement_id = Column(String(32), nullable=False, comment="结算主表ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ order_no = Column(String(50), nullable=False, comment="订单号")
+ supplier_id = Column(String(32), nullable=False, comment="供应商ID")
+ order_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="订单结算金额")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class OrdSupplierSettlementItemHis(Base):
+ """供应商结算明细历史表"""
+ __tablename__ = "ord_supplier_settlement_item_his"
+ __table_args__ = (PrimaryKeyConstraint("id", "his_create_at", name="pk_ord_supplier_settlement_item_his"),)
+
+ id = Column(String(32), nullable=False, comment="主键ID")
+ settlement_id = Column(String(32), nullable=False, comment="结算主表ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ order_no = Column(String(50), nullable=False, comment="订单号")
+ supplier_id = Column(String(32), nullable=False, comment="供应商ID")
+ order_amount = Column(Numeric(12, 2), nullable=False, default=0, comment="订单结算金额")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizRefund(Base):
+ """退款记录表"""
+ __tablename__ = "biz_refund"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ refund_no = Column(String(50), unique=True, nullable=False, comment="退款单编号")
+ refund_amount = Column(Numeric(12, 2), nullable=False, comment="退款金额")
+ refund_method = Column(String(20), nullable=False, comment="退款方式")
+ refund_no_wechat = Column(String(100), nullable=True, comment="微信退款单号")
+ refund_time = Column(DateTime, nullable=True, comment="退款时间")
+ refund_result = Column(String(20), default="success", comment="退款结果")
+ refund_fail_reason = Column(String(500), nullable=True, comment="退款失败原因")
+ status = Column(String(20), default="pending", comment="状态")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizSettlementHis(Base):
+ """结算记录历史表"""
+ __tablename__ = "biz_settlement_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ region_id = Column(String(32), nullable=False, comment="区域ID")
+ amount = Column(Numeric(12, 2), nullable=False, comment="结算金额")
+ settlement_method = Column(String(20), nullable=False, comment="结算方式")
+ settlement_time = Column(DateTime, nullable=True, comment="结算时间")
+ settlement_file_url = Column(String(500), nullable=True, comment="结算凭证文件URL")
+ settlement_note = Column(String(500), nullable=True, comment="结算备注")
+ status = Column(String(20), default="pending", comment="状态")
+ reviewed_by = Column(String(32), nullable=True, comment="审核人ID")
+ reviewed_time = Column(DateTime, nullable=True, comment="审核时间")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizRefundHis(Base):
+ """退款记录历史表"""
+ __tablename__ = "biz_refund_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ refund_no = Column(String(50), nullable=False, comment="退款单编号")
+ refund_amount = Column(Numeric(12, 2), nullable=False, comment="退款金额")
+ refund_method = Column(String(20), nullable=False, comment="退款方式")
+ refund_no_wechat = Column(String(100), nullable=True, comment="微信退款单号")
+ refund_time = Column(DateTime, nullable=True, comment="退款时间")
+ refund_result = Column(String(20), default="success", comment="退款结果")
+ refund_fail_reason = Column(String(500), nullable=True, comment="退款失败原因")
+ status = Column(String(20), default="pending", comment="状态")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizWechatPay(Base):
+ """微信支付订单表"""
+ __tablename__ = "biz_wechat_pay"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ out_trade_no = Column(String(64), unique=True, nullable=False, comment="商户订单号")
+ transaction_id = Column(String(64), nullable=True, comment="微信支付订单号")
+ total_fee = Column(Integer, nullable=False, comment="支付金额(分)")
+ cash_fee = Column(Integer, nullable=True, comment="现金支付金额(分)")
+ pay_time = Column(DateTime, nullable=True, comment="支付时间")
+ trade_state = Column(String(20), nullable=True, comment="交易状态")
+ trade_state_desc = Column(String(200), nullable=True, comment="交易状态描述")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizAttachment(Base):
+ """附件表"""
+ __tablename__ = "biz_attachment"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ business_type = Column(String(50), nullable=False, comment="业务类型")
+ business_id = Column(String(32), nullable=False, comment="业务ID")
+ file_name = Column(String(200), nullable=False, comment="文件名称")
+ file_url = Column(String(500), nullable=False, comment="文件URL")
+ file_size = Column(Integer, nullable=True, comment="文件大小")
+ file_type = Column(String(50), nullable=True, comment="文件类型")
+ upload_by = Column(String(32), nullable=True, comment="上传人ID")
+ upload_by_name = Column(String(100), nullable=True, comment="上传人姓名")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizOrderAdjust(Base):
+ """订单调整记录表"""
+ __tablename__ = "biz_order_adjust"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ order_item_id = Column(String(32), nullable=True, comment="订单项ID")
+ adjust_type = Column(String(20), nullable=False, comment="调整类型")
+ before_quantity = Column(Numeric(12, 4), nullable=True, comment="调整前数量")
+ after_quantity = Column(Numeric(12, 4), nullable=True, comment="调整后数量")
+ before_amount = Column(Numeric(12, 2), nullable=True, comment="调整前金额")
+ after_amount = Column(Numeric(12, 2), nullable=True, comment="调整后金额")
+ difference_amount = Column(Numeric(12, 2), default=0, comment="差异金额")
+ reason = Column(String(500), nullable=False, comment="调整原因")
+ snapshot_items_json = Column(Text, nullable=True, comment="调整后订单商品快照(JSON)")
+ substitute_product_id = Column(String(32), nullable=True, comment="替代商品ID")
+ operator_id = Column(String(32), nullable=False, comment="操作人ID")
+ operator_name = Column(String(100), nullable=False, comment="操作人姓名")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizOrderAdjustHis(Base):
+ """订单调整记录历史表"""
+ __tablename__ = "biz_order_adjust_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ order_item_id = Column(String(32), nullable=True, comment="订单项ID")
+ adjust_type = Column(String(20), nullable=False, comment="调整类型")
+ before_quantity = Column(Numeric(12, 4), nullable=True, comment="调整前数量")
+ after_quantity = Column(Numeric(12, 4), nullable=True, comment="调整后数量")
+ before_amount = Column(Numeric(12, 2), nullable=True, comment="调整前金额")
+ after_amount = Column(Numeric(12, 2), nullable=True, comment="调整后金额")
+ difference_amount = Column(Numeric(12, 2), default=0, comment="差异金额")
+ reason = Column(String(500), nullable=False, comment="调整原因")
+ snapshot_items_json = Column(Text, nullable=True, comment="调整后订单商品快照(JSON)")
+ substitute_product_id = Column(String(32), nullable=True, comment="替代商品ID")
+ operator_id = Column(String(32), nullable=False, comment="操作人ID")
+ operator_name = Column(String(100), nullable=False, comment="操作人姓名")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizInvoice(Base):
+ """发票表"""
+ __tablename__ = "biz_invoice"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ invoice_no = Column(String(50), nullable=False, unique=True, comment="发票号")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ order_no = Column(String(50), nullable=False, comment="订单号")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ client_name = Column(String(100), nullable=False, comment="客户名称")
+ invoice_type = Column(String(20), nullable=False, comment="发票类型: normal-普通发票, vat-增值税发票")
+ invoice_amount = Column(Numeric(12, 2), nullable=False, comment="发票金额")
+ tax_rate = Column(Numeric(5, 2), default=0, comment="税率")
+ tax_amount = Column(Numeric(12, 2), default=0, comment="税额")
+ title = Column(String(200), nullable=False, comment="发票抬头")
+ tax_no = Column(String(50), nullable=True, comment="税号")
+ address = Column(String(500), nullable=True, comment="开票地址")
+ phone = Column(String(20), nullable=True, comment="电话")
+ bank = Column(String(100), nullable=True, comment="开户银行")
+ bank_account = Column(String(50), nullable=True, comment="银行账号")
+ status = Column(String(20), nullable=False, default="pending", comment="状态: pending-待开票, issued-已开票, void-已作废")
+ invoice_url = Column(String(500), nullable=True, comment="发票文件URL")
+ issued_at = Column(DateTime, nullable=True, comment="开票时间")
+ issued_by = Column(String(32), nullable=True, comment="开票人ID")
+ issued_by_name = Column(String(100), nullable=True, comment="开票人姓名")
+ remark = Column(String(500), nullable=True, comment="备注")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizSystemConfig(Base):
+ """系统配置表"""
+ __tablename__ = "biz_system_config"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ config_key = Column(String(100), nullable=False, unique=True, comment="配置键")
+ config_value = Column(Text, nullable=True, comment="配置值")
+ config_type = Column(String(50), default="string", comment="配置类型: string, number, boolean, json")
+ config_name = Column(String(100), nullable=False, comment="配置名称")
+ config_group = Column(String(50), nullable=True, comment="配置分组")
+ description = Column(String(500), nullable=True, comment="描述")
+ is_active = Column(Integer, default=1, comment="是否启用")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizDelivererReview(Base):
+ """配送员评价表"""
+ __tablename__ = "biz_deliverer_review"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ task_id = Column(String(32), nullable=True, comment="配送任务ID")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ deliverer_id = Column(String(32), nullable=False, comment="配送员ID")
+ score = Column(Integer, nullable=False, comment="评分(1-5)")
+ tags = Column(String(500), nullable=True, comment="评价标签,逗号分隔")
+ content = Column(String(1000), nullable=True, comment="评价内容")
+ is_anonymous = Column(Boolean, default=False, comment="是否匿名")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizDelivererReviewHis(Base):
+ """配送员评价历史表"""
+ __tablename__ = "biz_deliverer_review_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ task_id = Column(String(32), nullable=True, comment="配送任务ID")
+ client_id = Column(String(32), nullable=False, comment="客户ID")
+ deliverer_id = Column(String(32), nullable=False, comment="配送员ID")
+ score = Column(Integer, nullable=False, comment="评分(1-5)")
+ tags = Column(String(500), nullable=True, comment="评价标签,逗号分隔")
+ content = Column(String(1000), nullable=True, comment="评价内容")
+ is_anonymous = Column(Boolean, default=False, comment="是否匿名")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizDeliveryWarning(Base):
+ """配送时效预警表"""
+ __tablename__ = "biz_delivery_warning"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ task_id = Column(String(32), nullable=False, comment="配送任务ID")
+ order_id = Column(String(32), nullable=True, comment="订单ID")
+ warning_type = Column(String(20), nullable=False, comment="预警类型")
+ urgency_level = Column(String(20), nullable=False, comment="紧急程度")
+ remaining_minutes = Column(Integer, nullable=True, comment="剩余分钟数")
+ message = Column(String(500), nullable=True, comment="预警消息")
+ is_sent = Column(Integer, default=0, comment="是否已发送通知")
+ sent_time = Column(DateTime, nullable=True, comment="发送时间")
+ is_resolved = Column(Integer, default=0, comment="是否已解决")
+ resolved_time = Column(DateTime, nullable=True, comment="解决时间")
+ resolved_by = Column(String(32), nullable=True, comment="解决人ID")
+ resolved_note = Column(String(500), nullable=True, comment="解决备注")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ is_del = Column(Integer, default=0, comment="是否删除")
+
+
+class BizDeliveryWarningHis(Base):
+ """配送时效预警历史表"""
+ __tablename__ = "biz_delivery_warning_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ task_id = Column(String(32), nullable=False, comment="配送任务ID")
+ order_id = Column(String(32), nullable=True, comment="订单ID")
+ warning_type = Column(String(20), nullable=False, comment="预警类型")
+ urgency_level = Column(String(20), nullable=False, comment="紧急程度")
+ remaining_minutes = Column(Integer, nullable=True, comment="剩余分钟数")
+ message = Column(String(500), nullable=True, comment="预警消息")
+ is_sent = Column(Integer, default=0, comment="是否已发送通知")
+ sent_time = Column(DateTime, nullable=True, comment="发送时间")
+ is_resolved = Column(Integer, default=0, comment="是否已解决")
+ resolved_time = Column(DateTime, nullable=True, comment="解决时间")
+ resolved_by = Column(String(32), nullable=True, comment="解决人ID")
+ resolved_note = Column(String(500), nullable=True, comment="解决备注")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class BizOrderStatusLogHis(Base):
+ """订单状态变更日志历史表"""
+ __tablename__ = "biz_order_status_log_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ order_id = Column(String(32), nullable=False, comment="订单ID")
+ from_status = Column(String(30), nullable=True, comment="变更前状态")
+ to_status = Column(String(30), nullable=False, comment="变更后状态")
+ operator_id = Column(String(32), nullable=False, comment="操作人ID")
+ operator_role = Column(String(20), nullable=False, comment="操作人角色")
+ reason = Column(String(500), nullable=True, comment="变更原因")
+ note = Column(String(1000), nullable=True, comment="备注")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class SysApiRequestLogHis(Base):
+ """API 请求日志历史表"""
+ __tablename__ = "sys_api_request_log_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ http_method = Column(String(10), nullable=False, comment="HTTP 方法")
+ path = Column(String(500), nullable=False, comment="请求路径")
+ query_string = Column(String(2000), nullable=True, comment="Query 字符串")
+ request_body = Column(Text, nullable=True, comment="请求体(截断)")
+ response_body = Column(Text, nullable=True, comment="响应体(截断)")
+ status_code = Column(Integer, nullable=False, comment="HTTP 状态码")
+ duration_ms = Column(Integer, nullable=False, comment="耗时(ms)")
+ user_id = Column(String(32), nullable=True, comment="用户ID")
+ username = Column(String(100), nullable=True, comment="用户名")
+ role = Column(String(20), nullable=True, comment="角色")
+ ip_address = Column(String(50), nullable=True, comment="IP地址")
+ user_agent = Column(String(500), nullable=True, comment="User-Agent")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class SysPasswordHistoryHis(Base):
+ """密码修改历史表(历史)"""
+ __tablename__ = "sys_password_history_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ user_id = Column(String(32), nullable=False, comment="用户ID")
+ operation_type = Column(String(20), nullable=False, comment="操作类型")
+ operator_id = Column(String(32), nullable=True, comment="操作人ID(重置时)")
+ operator_name = Column(String(100), nullable=True, comment="操作人姓名")
+ ip_address = Column(String(50), nullable=True, comment="IP地址")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+# =========================================
+# 扩展模块
+# =========================================
+
+class SysUpgradeLog(Base):
+ """系统更新日志表"""
+ __tablename__ = "sys_upgrade_log"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ version = Column(String(50), nullable=False, comment="版本号")
+ title = Column(String(200), nullable=False, comment="更新标题")
+ date = Column(DateTime, nullable=False, comment="更新日期")
+ detail = Column(Text, nullable=True, comment="更新详情(JSON数组)")
+ require_re_login = Column(Integer, default=0, comment="是否需要重新登录: 0-否, 1-是")
+ remark = Column(String(500), nullable=True, comment="备注")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class SysUpgradeLogHis(Base):
+ """系统更新日志历史表"""
+ __tablename__ = "sys_upgrade_log_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ version = Column(String(50), nullable=False, comment="版本号")
+ title = Column(String(200), nullable=False, comment="更新标题")
+ date = Column(DateTime, nullable=False, comment="更新日期")
+ detail = Column(Text, nullable=True, comment="更新详情(JSON数组)")
+ require_re_login = Column(Integer, default=0, comment="是否需要重新登录: 0-否, 1-是")
+ remark = Column(String(500), nullable=True, comment="备注")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
+
+
+class SysComment(Base):
+ """系统评论表"""
+ __tablename__ = "sys_comment"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ parent_id = Column(String(32), nullable=True, comment="父评论ID")
+ author = Column(String(100), nullable=False, comment="评论者")
+ author_id = Column(String(32), nullable=True, comment="评论者ID")
+ content = Column(String(1000), nullable=False, comment="评论内容")
+ target_type = Column(String(50), nullable=True, comment="目标类型")
+ target_id = Column(String(32), nullable=True, comment="目标ID")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizClientCart(Base):
+ """客户购物车"""
+ __tablename__ = "biz_client_cart"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ client_id = Column(String(32), nullable=False, comment="客户用户ID")
+ price_list_id = Column(String(32), nullable=False, comment="价格清单ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ unit_id = Column(String(32), nullable=False, comment="单位ID")
+ quantity = Column(Numeric(12, 4), nullable=False, default=0, comment="数量")
+ unit_price = Column(Numeric(12, 2), nullable=False, default=0, comment="单价(元)")
+ amount = Column(Numeric(12, 2), nullable=False, default=0, comment="行金额(元)")
+ product_name = Column(String(200), nullable=True, comment="商品名称快照")
+ unit_name = Column(String(50), nullable=True, comment="单位名称快照")
+ category_name = Column(String(100), nullable=True, comment="分类名称快照")
+ image_url = Column(String(500), nullable=True, comment="图片")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizClientCartHis(Base):
+ """客户购物车历史"""
+ __tablename__ = "biz_client_cart_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ client_id = Column(String(32), nullable=False, comment="客户用户ID")
+ price_list_id = Column(String(32), nullable=False, comment="价格清单ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ unit_id = Column(String(32), nullable=False, comment="单位ID")
+ quantity = Column(Numeric(12, 4), nullable=False, default=0, comment="数量")
+ unit_price = Column(Numeric(12, 2), nullable=False, default=0, comment="单价(元)")
+ amount = Column(Numeric(12, 2), nullable=False, default=0, comment="行金额(元)")
+ product_name = Column(String(200), nullable=True, comment="商品名称快照")
+ unit_name = Column(String(50), nullable=True, comment="单位名称快照")
+ category_name = Column(String(100), nullable=True, comment="分类名称快照")
+ image_url = Column(String(500), nullable=True, comment="图片")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
diff --git a/backend/app/models/order.py b/backend/app/models/order.py
new file mode 100644
index 00000000..ebca4c60
--- /dev/null
+++ b/backend/app/models/order.py
@@ -0,0 +1,166 @@
+"""订单相关模型"""
+from datetime import datetime
+
+from sqlalchemy import Boolean, Column, DateTime, Integer, String, Enum as SQLEnum, Numeric, Text, ForeignKey
+from sqlalchemy.orm import relationship
+
+from app.core.database import Base
+import enum
+
+
+class OrderStatus(str, enum.Enum):
+ """订单主状态"""
+ PENDING_PAYMENT = "pending_payment" # 待支付
+ PENDING_CONFIRM = "pending_confirm" # 待客户确认收货
+ COMPLETED = "completed" # 已完结
+ CANCELLED = "cancelled" # 已取消
+
+
+class PaymentMethod(str, enum.Enum):
+ """支付方式"""
+ WECHAT = "wechat" # 微信支付
+ OFFLINE = "offline" # 线下结算
+
+
+class Order(Base):
+ """订单主表"""
+ __tablename__ = "orders"
+
+ id = Column(Integer, primary_key=True, index=True, autoincrement=True)
+ order_no = Column(String(50), unique=True, nullable=False, comment="订单编号")
+ region_id = Column(Integer, nullable=False, comment="区域ID")
+ client_id = Column(Integer, nullable=False, comment="客户ID")
+ price_list_id = Column(Integer, nullable=False, comment="使用的价格清单ID")
+ price_version = Column(Integer, nullable=False, comment="价格版本号")
+
+ status = Column(SQLEnum(OrderStatus), default=OrderStatus.PENDING_PAYMENT, comment="订单状态")
+ payment_method = Column(SQLEnum(PaymentMethod), nullable=True, comment="支付方式")
+
+ total_amount = Column(Numeric(12, 2), default=0, comment="订单总金额")
+ paid_amount = Column(Numeric(12, 2), default=0, comment="已付金额")
+ receivable_amount = Column(Numeric(12, 2), default=0, comment="应收金额")
+ refund_amount = Column(Numeric(12, 2), default=0, comment="退款金额")
+
+ contact_person = Column(String(100), nullable=True, comment="联系人")
+ contact_phone = Column(String(20), nullable=True, comment="联系电话")
+ delivery_address = Column(String(500), nullable=True, comment="配送地址")
+
+ cancel_reason = Column(String(500), nullable=True, comment="取消原因")
+ cancel_review_time = Column(DateTime, nullable=True, comment="取消审核时间")
+ cancel_reviewed_by = Column(Integer, nullable=True, comment="取消审核人ID")
+ cancel_review_note = Column(String(500), nullable=True, comment="取消审核备注")
+
+ completed_time = Column(DateTime, nullable=True, comment="完结时间")
+ completed_by = Column(Integer, nullable=True, comment="完结人ID")
+
+ remark = Column(Text, nullable=True, comment="订单备注")
+
+ payment_time = Column(DateTime, nullable=True, comment="支付时间")
+ delivery_assign_time = Column(DateTime, nullable=True, comment="配送分配时间")
+
+ created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间")
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
+
+ # 关联
+ client = relationship("Client", back_populates="orders")
+ items = relationship("OrderItem", back_populates="order")
+ delivery_task = relationship("DeliveryTask", back_populates="order", uselist=False)
+ status_logs = relationship("OrderStatusLog", back_populates="order")
+
+
+class OrderItem(Base):
+ """订单项表"""
+ __tablename__ = "order_items"
+
+ id = Column(Integer, primary_key=True, index=True, autoincrement=True)
+ order_id = Column(Integer, nullable=False, comment="订单ID")
+ product_id = Column(Integer, nullable=False, comment="商品ID")
+ unit_id = Column(Integer, nullable=False, comment="单位ID")
+
+ quantity = Column(Numeric(12, 4), default=0, comment="数量")
+ unit_price = Column(Numeric(10, 2), nullable=False, comment="单价")
+ amount = Column(Numeric(12, 2), default=0, comment="金额")
+
+ delivered_quantity = Column(Numeric(12, 4), default=0, comment="已配送数量")
+
+ status = Column(String(20), default="normal", comment="项状态:normal-正常,reduced-已减量,deleted-已删除,substituted-已替代")
+ note = Column(String(500), nullable=True, comment="备注(如缺货原因)")
+
+ created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间")
+
+ order = relationship("Order", back_populates="items")
+ product = relationship("Product", back_populates="order_items")
+
+
+class OrderStatusLog(Base):
+ """订单状态变更日志"""
+ __tablename__ = "order_status_logs"
+
+ id = Column(Integer, primary_key=True, index=True, autoincrement=True)
+ order_id = Column(Integer, nullable=False, comment="订单ID")
+ from_status = Column(String(30), nullable=True, comment="变更前状态")
+ to_status = Column(String(30), nullable=False, comment="变更后状态")
+ operator_id = Column(Integer, nullable=False, comment="操作人ID")
+ operator_role = Column(String(20), nullable=False, comment="操作人角色")
+ reason = Column(String(500), nullable=True, comment="变更原因")
+ note = Column(String(1000), nullable=True, comment="备注")
+ created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间")
+
+ order = relationship("Order", back_populates="status_logs")
+
+
+class DeliveryTask(Base):
+ """配送任务表"""
+ __tablename__ = "delivery_tasks"
+
+ id = Column(Integer, primary_key=True, index=True, autoincrement=True)
+ task_no = Column(String(50), unique=True, nullable=False, comment="配送单编号")
+ order_id = Column(Integer, nullable=False, comment="订单ID")
+ region_id = Column(Integer, nullable=False, comment="区域ID")
+ supplier_id = Column(Integer, nullable=False, comment="供应商ID")
+ deliver_id = Column(Integer, nullable=True, comment="配送员ID")
+
+ status = Column(String(20), default="pending", comment="状态:pending-待取货,picking-取货中,delivering-配送中,completed-已完成,cancelled-已取消")
+ deliverer_status = Column(String(20), default="pending_pickup", comment="配送员状态: pending_pickup/delivering/delivered")
+ supplier_prepare_status = Column(String(20), default="pending_prepare", comment="供应商备货状态: pending_prepare/prepared/picked_up")
+
+ pickup_time = Column(DateTime, nullable=True, comment="取货时间")
+ pickup_remark = Column(String(500), nullable=True, comment="取货备注")
+ pickup_image_url = Column(String(500), nullable=True, comment="取货凭证图片")
+
+ delivery_time = Column(DateTime, nullable=True, comment="送达时间")
+ delivery_remark = Column(String(500), nullable=True, comment="送达备注")
+ delivery_image_url = Column(String(500), nullable=True, comment="送达凭证图片")
+
+ expire_warning_sent = Column(Boolean, default=False, comment="是否已发送过期预警")
+
+ created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间")
+ updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间")
+
+ # 关联
+ order = relationship("Order", back_populates="delivery_task")
+ supplier = relationship("Supplier", back_populates="delivery_tasks")
+ deliver = relationship("Deliver", back_populates="delivery_tasks")
+
+
+class Settlement(Base):
+ """线下结算记录表"""
+ __tablename__ = "settlements"
+
+ id = Column(Integer, primary_key=True, index=True, autoincrement=True)
+ order_id = Column(Integer, nullable=False, comment="订单ID")
+ client_id = Column(Integer, nullable=False, comment="客户ID")
+ region_id = Column(Integer, nullable=False, comment="区域ID")
+
+ amount = Column(Numeric(12, 2), nullable=False, comment="结算金额")
+ settlement_method = Column(String(20), nullable=False, comment="结算方式:transfer-转账,cash-现金,other-其他")
+ settlement_time = Column(DateTime, nullable=True, comment="结算时间")
+ settlement_file_url = Column(String(500), nullable=True, comment="结算凭证文件URL")
+ settlement_note = Column(String(500), nullable=True, comment="结算备注")
+
+ status = Column(String(20), default="pending", comment="状态:pending-待确认,completed-已完成")
+ reviewed_by = Column(Integer, nullable=True, comment="审核人ID")
+ reviewed_time = Column(DateTime, nullable=True, comment="审核时间")
+
+ created_by = Column(Integer, nullable=False, comment="创建人ID")
+ created_at = Column(DateTime, default=datetime.utcnow, comment="创建时间")
diff --git a/backend/app/models/product_banner.py b/backend/app/models/product_banner.py
new file mode 100644
index 00000000..4bed8a22
--- /dev/null
+++ b/backend/app/models/product_banner.py
@@ -0,0 +1,42 @@
+"""商品轮播图模型"""
+from datetime import datetime
+
+from sqlalchemy import Column, String, DateTime, Integer, Text
+
+from app.core.database import Base
+
+
+class BasProductBanner(Base):
+ """商品轮播图表"""
+ __tablename__ = "bas_product_banner"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ media_type = Column(String(20), nullable=False, default="image", comment="媒体类型:image-图片, video-视频")
+ file_url = Column(String(500), nullable=False, comment="文件URL地址")
+ thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL")
+ sort_order = Column(Integer, default=0, comment="排序")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BasProductBannerHis(Base):
+ """商品轮播图历史表"""
+ __tablename__ = "bas_product_banner_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ product_id = Column(String(32), nullable=True, comment="商品ID")
+ media_type = Column(String(20), default="image", comment="媒体类型")
+ file_url = Column(String(500), nullable=True, comment="文件URL地址")
+ thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL")
+ sort_order = Column(Integer, default=0, comment="排序")
+ create_at = Column(DateTime, nullable=True, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
diff --git a/backend/app/models/product_detail_media.py b/backend/app/models/product_detail_media.py
new file mode 100644
index 00000000..7080d219
--- /dev/null
+++ b/backend/app/models/product_detail_media.py
@@ -0,0 +1,46 @@
+"""商品详情媒体模型"""
+from datetime import datetime
+
+from sqlalchemy import Column, String, DateTime, Integer, Text
+
+from app.core.database import Base
+
+
+class BasProductDetailMedia(Base):
+ """商品详情媒体表"""
+ __tablename__ = "bas_product_detail_media"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ content_type = Column(String(20), nullable=False, default="image", comment="内容类型:image-图片, video-视频, text-文本")
+ content_text = Column(Text, nullable=True, comment="文本内容")
+ file_url = Column(String(500), nullable=True, comment="文件URL地址")
+ thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL")
+ description = Column(String(500), nullable=True, comment="描述")
+ sort_order = Column(Integer, default=0, comment="排序")
+ is_del = Column(Integer, default=0, comment="是否删除")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BasProductDetailMediaHis(Base):
+ """商品详情媒体历史表"""
+ __tablename__ = "bas_product_detail_media_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ product_id = Column(String(32), nullable=True, comment="商品ID")
+ content_type = Column(String(20), default="image", comment="内容类型")
+ content_text = Column(Text, nullable=True, comment="文本内容")
+ file_url = Column(String(500), nullable=True, comment="文件URL地址")
+ thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL")
+ description = Column(String(500), nullable=True, comment="描述")
+ sort_order = Column(Integer, default=0, comment="排序")
+ create_at = Column(DateTime, nullable=True, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
diff --git a/backend/app/models/product_parameter.py b/backend/app/models/product_parameter.py
new file mode 100644
index 00000000..5c87b12c
--- /dev/null
+++ b/backend/app/models/product_parameter.py
@@ -0,0 +1,50 @@
+"""商品参数码表模型"""
+from datetime import datetime
+
+from sqlalchemy import Column, String, DateTime, Integer, Boolean, Text
+
+from app.core.database import Base
+
+
+class BasProductParameter(Base):
+ """商品参数码表"""
+ __tablename__ = "bas_product_parameter"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ name = Column(String(100), nullable=False, comment="参数名称")
+ code = Column(String(50), nullable=False, unique=True, comment="参数编码")
+ param_type = Column(String(20), nullable=False, default="text", comment="参数类型:text-文本输入, select-单选下拉, multi_select-多选")
+ options = Column(Text, nullable=True, comment="可选值列表,JSON数组")
+ placeholder = Column(String(100), nullable=True, comment="输入提示文字")
+ is_required = Column(Integer, default=0, comment="是否必填:0-否, 1-是")
+ is_active = Column(Integer, default=1, comment="是否启用:0-禁用, 1-启用")
+ is_del = Column(Integer, default=0, comment="是否删除:0-否, 1-是")
+ sort_order = Column(Integer, default=0, comment="排序")
+ remark = Column(String(500), nullable=True, comment="备注说明")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BasProductParameterHis(Base):
+ """商品参数码表历史表"""
+ __tablename__ = "bas_product_parameter_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ name = Column(String(100), nullable=True, comment="参数名称")
+ code = Column(String(50), nullable=True, comment="参数编码")
+ param_type = Column(String(20), default="text", comment="参数类型")
+ options = Column(Text, nullable=True, comment="可选值列表")
+ placeholder = Column(String(100), nullable=True, comment="输入提示文字")
+ is_required = Column(Integer, default=0, comment="是否必填")
+ is_active = Column(Integer, default=1, comment="是否启用")
+ sort_order = Column(Integer, default=0, comment="排序")
+ remark = Column(String(500), nullable=True, comment="备注说明")
+ create_at = Column(DateTime, nullable=True, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
diff --git a/backend/app/models/product_parameter_value.py b/backend/app/models/product_parameter_value.py
new file mode 100644
index 00000000..b3e6dfe0
--- /dev/null
+++ b/backend/app/models/product_parameter_value.py
@@ -0,0 +1,46 @@
+"""商品参数值模型"""
+from datetime import datetime
+
+from sqlalchemy import Column, String, DateTime, Integer, Text
+
+from app.core.database import Base
+
+
+class BasProductParameterValue(Base):
+ """商品参数值表"""
+ __tablename__ = "bas_product_parameter_value"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ parameter_id = Column(String(32), nullable=False, comment="参数ID")
+ parameter_name = Column(String(100), nullable=True, comment="参数名称快照")
+ parameter_code = Column(String(50), nullable=True, comment="参数编码快照")
+ param_type = Column(String(20), nullable=True, comment="参数类型快照")
+ param_value = Column(Text, nullable=True, comment="参数值")
+ sort_order = Column(Integer, default=0, comment="排序")
+ is_del = Column(Integer, default=0, comment="是否删除:0-否, 1-是")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BasProductParameterValueHis(Base):
+ """商品参数值表历史表"""
+ __tablename__ = "bas_product_parameter_value_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ product_id = Column(String(32), nullable=True, comment="商品ID")
+ parameter_id = Column(String(32), nullable=True, comment="参数ID")
+ parameter_name = Column(String(100), nullable=True, comment="参数名称快照")
+ parameter_code = Column(String(50), nullable=True, comment="参数编码快照")
+ param_type = Column(String(20), nullable=True, comment="参数类型快照")
+ param_value = Column(Text, nullable=True, comment="参数值")
+ sort_order = Column(Integer, default=0, comment="排序")
+ create_at = Column(DateTime, nullable=True, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
diff --git a/backend/app/models/product_review.py b/backend/app/models/product_review.py
new file mode 100644
index 00000000..7e70c0f8
--- /dev/null
+++ b/backend/app/models/product_review.py
@@ -0,0 +1,66 @@
+"""售后评价模型"""
+from datetime import datetime
+
+from sqlalchemy import Column, String, DateTime, Integer, Text
+
+from app.core.database import Base
+
+
+class BizProductReview(Base):
+ """商品售后评价表"""
+ __tablename__ = "biz_product_review"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ product_id = Column(String(32), nullable=False, comment="商品ID")
+ order_id = Column(String(32), nullable=True, comment="订单ID")
+ order_item_id = Column(String(32), nullable=True, comment="订单项ID")
+ user_id = Column(String(32), nullable=False, comment="评价用户ID")
+ user_name = Column(String(100), nullable=True, comment="评价用户姓名")
+ rating = Column(Integer, nullable=False, default=5, comment="评分(1-5星)")
+ rating_tags = Column(String(500), nullable=True, comment="评价标签,多个逗号分隔")
+ content = Column(Text, nullable=True, comment="评价内容")
+ is_anonymous = Column(Integer, default=0, comment="是否匿名:0-否, 1-是")
+ reply_content = Column(Text, nullable=True, comment="商家回复内容")
+ reply_time = Column(DateTime, nullable=True, comment="商家回复时间")
+ reply_by = Column(String(32), nullable=True, comment="回复人ID")
+ reply_by_name = Column(String(100), nullable=True, comment="回复人姓名")
+ like_count = Column(Integer, default=0, comment="点赞数")
+ is_top = Column(Integer, default=0, comment="是否置顶:0-否, 1-是")
+ status = Column(String(20), default="pending", comment="状态:pending-待审核, approved-已通过, rejected-已拒绝")
+ reject_reason = Column(String(500), nullable=True, comment="拒绝原因")
+ is_del = Column(Integer, default=0, comment="是否删除:0-否, 1-是")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizProductReviewHis(Base):
+ """商品售后评价历史表"""
+ __tablename__ = "biz_product_review_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ product_id = Column(String(32), nullable=True, comment="商品ID")
+ order_id = Column(String(32), nullable=True, comment="订单ID")
+ order_item_id = Column(String(32), nullable=True, comment="订单项ID")
+ user_id = Column(String(32), nullable=True, comment="评价用户ID")
+ user_name = Column(String(100), nullable=True, comment="评价用户姓名")
+ rating = Column(Integer, default=5, comment="评分")
+ rating_tags = Column(String(500), nullable=True, comment="评价标签")
+ content = Column(Text, nullable=True, comment="评价内容")
+ is_anonymous = Column(Integer, default=0, comment="是否匿名")
+ reply_content = Column(Text, nullable=True, comment="商家回复内容")
+ reply_time = Column(DateTime, nullable=True, comment="商家回复时间")
+ reply_by = Column(String(32), nullable=True, comment="回复人ID")
+ reply_by_name = Column(String(100), nullable=True, comment="回复人姓名")
+ like_count = Column(Integer, default=0, comment="点赞数")
+ is_top = Column(Integer, default=0, comment="是否置顶")
+ status = Column(String(20), default="pending", comment="状态")
+ reject_reason = Column(String(500), nullable=True, comment="拒绝原因")
+ create_at = Column(DateTime, nullable=True, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
diff --git a/backend/app/models/product_review_media.py b/backend/app/models/product_review_media.py
new file mode 100644
index 00000000..da11d992
--- /dev/null
+++ b/backend/app/models/product_review_media.py
@@ -0,0 +1,42 @@
+"""售后评价媒体模型"""
+from datetime import datetime
+
+from sqlalchemy import Column, String, DateTime, Integer
+
+from app.core.database import Base
+
+
+class BizProductReviewMedia(Base):
+ """商品评价媒体表"""
+ __tablename__ = "biz_product_review_media"
+
+ id = Column(String(32), primary_key=True, comment="主键ID")
+ review_id = Column(String(32), nullable=False, comment="评价ID")
+ media_type = Column(String(20), nullable=False, default="image", comment="媒体类型:image-图片, video-视频")
+ file_url = Column(String(500), nullable=False, comment="文件URL地址")
+ thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL(视频时使用)")
+ sort_order = Column(Integer, default=0, comment="排序(数字越小越靠前)")
+ is_del = Column(Integer, default=0, comment="是否删除:0-否, 1-是")
+ create_at = Column(DateTime, default=datetime.now, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+
+
+class BizProductReviewMediaHis(Base):
+ """商品评价媒体历史表"""
+ __tablename__ = "biz_product_review_media_his"
+
+ id = Column(String(32), primary_key=True, nullable=False, comment="主键ID")
+ review_id = Column(String(32), nullable=True, comment="评价ID")
+ media_type = Column(String(20), default="image", comment="媒体类型")
+ file_url = Column(String(500), nullable=True, comment="文件URL地址")
+ thumbnail_url = Column(String(500), nullable=True, comment="缩略图URL")
+ sort_order = Column(Integer, default=0, comment="排序")
+ create_at = Column(DateTime, nullable=True, comment="创建时间")
+ create_by = Column(String(32), nullable=True, comment="创建人ID")
+ create_name = Column(String(100), nullable=True, comment="创建人姓名")
+ update_at = Column(DateTime, nullable=True, comment="修改时间")
+ update_by = Column(String(32), nullable=True, comment="修改人ID")
+ his_create_at = Column(DateTime, default=datetime.now, comment="历史记录创建时间")
diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py
new file mode 100644
index 00000000..62441921
--- /dev/null
+++ b/backend/app/schemas/__init__.py
@@ -0,0 +1,352 @@
+"""Schema模块"""
+from app.schemas.common import (
+ PaginationParams,
+ PaginationResult,
+ IdInput,
+ StatusInput,
+ DeleteInput
+)
+from app.schemas.region import (
+ RegionBase,
+ RegionCreate,
+ RegionUpdate,
+ RegionResponse,
+ RegionListResponse
+)
+from app.schemas.category import (
+ CategoryBase,
+ CategoryCreate,
+ CategoryUpdate,
+ CategoryTreeNode,
+ CategoryResponse,
+ CategoryListResponse
+)
+from app.schemas.unit import (
+ UnitBase,
+ UnitCreate,
+ UnitUpdate,
+ UnitResponse,
+ UnitListResponse
+)
+from app.schemas.user import (
+ UserBase,
+ UserCreate,
+ UserUpdate,
+ UserResponse,
+ UserListResponse,
+ LoginRequest,
+ LoginResponse,
+ ChangePasswordRequest,
+ ResetPasswordRequest
+)
+from app.schemas.product import (
+ ProductUnitBase,
+ ProductUnitCreate,
+ ProductUnitResponse,
+ ProductCategoryBase,
+ ProductCategoryCreate,
+ ProductCategoryResponse,
+ ProductBase,
+ ProductCreate,
+ ProductUpdate,
+ ProductResponse
+)
+from app.schemas.product_banner import (
+ ProductBannerBase,
+ ProductBannerCreate,
+ ProductBannerCreateBatch,
+ ProductBannerUpdate,
+ ProductBannerSortUpdate,
+ ProductBannerResponse,
+ ProductBannerListResponse
+)
+from app.schemas.product_detail_media import (
+ ProductDetailMediaBase,
+ ProductDetailMediaCreate,
+ ProductDetailMediaCreateBatch,
+ ProductDetailMediaUpdate,
+ ProductDetailMediaResponse,
+ ProductDetailMediaListResponse
+)
+from app.schemas.product_parameter import (
+ ProductParameterBase,
+ ProductParameterCreate,
+ ProductParameterUpdate,
+ ProductParameterResponse,
+ ProductParameterListResponse,
+ ProductParameterSimpleResponse
+)
+from app.schemas.category_parameter import (
+ CategoryParameterBase,
+ CategoryParameterItem,
+ CategoryParameterCreate,
+ CategoryParameterUpdate,
+ CategoryParameterBatchUpdate,
+ CategoryParameterResponse,
+ CategoryParameterWithDetailResponse,
+ CategoryParameterListResponse
+)
+from app.schemas.product_parameter_value import (
+ ProductParameterValueBase,
+ ProductParameterValueCreate,
+ ProductParameterValueUpdate,
+ ProductParameterValueBatchSave,
+ ProductParameterValueResponse,
+ ProductParameterValueListResponse,
+ ProductParameterValueWithParameterResponse
+)
+from app.schemas.product_review import (
+ ProductReviewBase,
+ ProductReviewCreate,
+ ProductReviewUpdate,
+ ProductReviewReplyRequest,
+ ProductReviewRejectRequest,
+ ProductReviewTopRequest,
+ ProductReviewResponse,
+ ProductReviewDetailResponse,
+ ProductReviewListResponse,
+ ProductReviewStatisticsResponse,
+ ProductReviewAdminListRequest,
+ MiniReviewListRequest
+)
+from app.schemas.product_review_media import (
+ ProductReviewMediaBase,
+ ProductReviewMediaCreate,
+ ProductReviewMediaCreateBatch,
+ ProductReviewMediaUpdate,
+ ProductReviewMediaResponse,
+ ProductReviewMediaListResponse
+)
+from app.schemas.inquiry import (
+ InquiryBase,
+ InquiryCreate,
+ InquiryUpdate,
+ InquiryResponse,
+ InquiryDetailResponse,
+ InquiryListResponse,
+ InquiryItemBase,
+ InquiryItemCreate,
+ InquiryItemResponse,
+ InquiryStatusResponse
+)
+from app.schemas.quotation import (
+ QuotationBase,
+ QuotationCreate,
+ QuotationUpdate,
+ QuotationResponse,
+ QuotationDetailResponse,
+ QuotationListResponse,
+ QuotationItemBase,
+ QuotationItemCreate,
+ QuotationItemResponse,
+ QuotationStatusResponse,
+ QuotationWithSupplierResponse,
+ QuotationWithItemsResponse
+)
+from app.schemas.price_list import (
+ PriceListBase,
+ PriceListCreate,
+ PriceListUpdate,
+ PriceListResponse,
+ PriceListDetailResponse,
+ PriceListListResponse,
+ PriceListItemBase,
+ PriceListItemCreate,
+ PriceListItemResponse,
+ PriceListPublishRequest,
+ PriceListInvalidateRequest,
+ PriceListStatusResponse,
+ ProductPriceResponse,
+ ProductPriceHistoryItem,
+ ProductPriceHistoryResponse,
+ PriceListGenerateRequest
+)
+from app.schemas.order import (
+ OrderCreate,
+ OrderPreviewRequest,
+ OrderResponse,
+ OrderDetailResponse,
+ OrderListResponse,
+ OrderPreviewResponse,
+ OrderItemCreate,
+ OrderItemResponse,
+ OrderCancelRequest,
+ OrderDirectCancelRequest,
+ OrderReviewCancelRequest,
+ OrderStatusResponse,
+ OrderStatusLogResponse,
+ DeliveryProgressResponse
+)
+from app.schemas.delivery import (
+ DeliveryTaskResponse,
+ DeliveryTaskDetailResponse,
+ DeliveryTaskListResponse,
+ DeliveryWarningListResponse,
+ DeliveryTaskItemResponse,
+ DeliveryAssignRequest,
+ DeliveryAssignBatchRequest,
+ DeliveryPickupRequest,
+ DeliveryDeliverItem,
+ DeliveryDeliverRequest,
+ DeliveryCancelRequest,
+ DeliveryReassignRequest,
+ DeliveryStatusResponse,
+ DeliveryTaskStatistics,
+ DeliveryAttachmentResponse,
+ DeliveryCompleteRequest
+)
+
+__all__ = [
+ "PaginationParams",
+ "PaginationResult",
+ "IdInput",
+ "StatusInput",
+ "DeleteInput",
+ "RegionBase",
+ "RegionCreate",
+ "RegionUpdate",
+ "RegionResponse",
+ "RegionListResponse",
+ "CategoryBase",
+ "CategoryCreate",
+ "CategoryUpdate",
+ "CategoryTreeNode",
+ "CategoryResponse",
+ "CategoryListResponse",
+ "UnitBase",
+ "UnitCreate",
+ "UnitUpdate",
+ "UnitResponse",
+ "UnitListResponse",
+ "UserBase",
+ "UserCreate",
+ "UserUpdate",
+ "UserResponse",
+ "UserListResponse",
+ "LoginRequest",
+ "LoginResponse",
+ "ChangePasswordRequest",
+ "ResetPasswordRequest",
+ "InquiryBase",
+ "InquiryCreate",
+ "InquiryUpdate",
+ "InquiryResponse",
+ "InquiryDetailResponse",
+ "InquiryListResponse",
+ "InquiryItemBase",
+ "InquiryItemCreate",
+ "InquiryItemResponse",
+ "InquiryStatusResponse",
+ "QuotationBase",
+ "QuotationCreate",
+ "QuotationUpdate",
+ "QuotationResponse",
+ "QuotationDetailResponse",
+ "QuotationListResponse",
+ "QuotationItemBase",
+ "QuotationItemCreate",
+ "QuotationItemResponse",
+ "QuotationStatusResponse",
+ "QuotationWithSupplierResponse",
+ "QuotationWithItemsResponse",
+ "PriceListBase",
+ "PriceListCreate",
+ "PriceListUpdate",
+ "PriceListResponse",
+ "PriceListDetailResponse",
+ "PriceListListResponse",
+ "PriceListItemBase",
+ "PriceListItemCreate",
+ "PriceListItemResponse",
+ "PriceListPublishRequest",
+ "PriceListInvalidateRequest",
+ "PriceListStatusResponse",
+ "ProductPriceResponse",
+ "ProductPriceHistoryItem",
+ "ProductPriceHistoryResponse",
+ "PriceListGenerateRequest",
+ "OrderCreate",
+ "OrderPreviewRequest",
+ "OrderResponse",
+ "OrderDetailResponse",
+ "OrderListResponse",
+ "OrderPreviewResponse",
+ "OrderItemCreate",
+ "OrderItemResponse",
+ "OrderCancelRequest",
+ "OrderDirectCancelRequest",
+ "OrderReviewCancelRequest",
+ "OrderStatusResponse",
+ "OrderStatusLogResponse",
+ "DeliveryProgressResponse",
+ "DeliveryTaskResponse",
+ "DeliveryTaskDetailResponse",
+ "DeliveryTaskListResponse",
+ "DeliveryWarningListResponse",
+ "DeliveryTaskItemResponse",
+ "DeliveryAssignRequest",
+ "DeliveryAssignBatchRequest",
+ "DeliveryPickupRequest",
+ "DeliveryDeliverItem",
+ "DeliveryDeliverRequest",
+ "DeliveryCancelRequest",
+ "DeliveryReassignRequest",
+ "DeliveryStatusResponse",
+ "DeliveryTaskStatistics",
+ "DeliveryAttachmentResponse",
+ # 商品多媒体
+ "ProductBannerBase",
+ "ProductBannerCreate",
+ "ProductBannerCreateBatch",
+ "ProductBannerUpdate",
+ "ProductBannerSortUpdate",
+ "ProductBannerResponse",
+ "ProductBannerListResponse",
+ "ProductDetailMediaBase",
+ "ProductDetailMediaCreate",
+ "ProductDetailMediaCreateBatch",
+ "ProductDetailMediaUpdate",
+ "ProductDetailMediaResponse",
+ "ProductDetailMediaListResponse",
+ # 商品参数
+ "ProductParameterBase",
+ "ProductParameterCreate",
+ "ProductParameterUpdate",
+ "ProductParameterResponse",
+ "ProductParameterListResponse",
+ "ProductParameterSimpleResponse",
+ "CategoryParameterBase",
+ "CategoryParameterItem",
+ "CategoryParameterCreate",
+ "CategoryParameterUpdate",
+ "CategoryParameterBatchUpdate",
+ "CategoryParameterResponse",
+ "CategoryParameterWithDetailResponse",
+ "CategoryParameterListResponse",
+ "ProductParameterValueBase",
+ "ProductParameterValueCreate",
+ "ProductParameterValueUpdate",
+ "ProductParameterValueBatchSave",
+ "ProductParameterValueResponse",
+ "ProductParameterValueListResponse",
+ "ProductParameterValueWithParameterResponse",
+ # 售后评价
+ "ProductReviewBase",
+ "ProductReviewCreate",
+ "ProductReviewUpdate",
+ "ProductReviewReplyRequest",
+ "ProductReviewRejectRequest",
+ "ProductReviewTopRequest",
+ "ProductReviewResponse",
+ "ProductReviewDetailResponse",
+ "ProductReviewListResponse",
+ "ProductReviewStatisticsResponse",
+ "ProductReviewAdminListRequest",
+ "MiniReviewListRequest",
+ "ProductReviewMediaBase",
+ "ProductReviewMediaCreate",
+ "ProductReviewMediaCreateBatch",
+ "ProductReviewMediaUpdate",
+ "ProductReviewMediaResponse",
+ "ProductReviewMediaListResponse",
+]
diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py
new file mode 100644
index 00000000..46351ef9
--- /dev/null
+++ b/backend/app/schemas/auth.py
@@ -0,0 +1,56 @@
+"""Pydantic schemas - 用户认证"""
+from pydantic import BaseModel, Field
+from typing import Optional
+from datetime import datetime
+
+
+class LoginRequest(BaseModel):
+ """登录请求"""
+ username: str = Field(..., min_length=3, max_length=50)
+ password: str = Field(..., min_length=6)
+
+
+class LoginResponse(BaseModel):
+ """登录响应"""
+ access_token: str
+ token_type: str = "bearer"
+ user_id: int
+ username: str
+ role: str
+ real_name: Optional[str] = None
+
+
+class UserCreate(BaseModel):
+ """创建用户请求"""
+ username: str = Field(..., min_length=3, max_length=50)
+ password: str = Field(..., min_length=6)
+ real_name: Optional[str] = None
+ phone: Optional[str] = None
+ role: str
+ region_id: Optional[int] = None
+
+
+class UserResponse(BaseModel):
+ """用户响应"""
+ id: int
+ username: str
+ real_name: Optional[str] = None
+ phone: Optional[str] = None
+ role: str
+ region_id: Optional[int] = None
+ is_active: bool
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+
+
+class ChangePasswordRequest(BaseModel):
+ """修改密码请求"""
+ old_password: str
+ new_password: str = Field(..., min_length=6)
+
+
+class ResetPasswordRequest(BaseModel):
+ """重置密码请求(运营操作)"""
+ user_id: int
diff --git a/backend/app/schemas/category.py b/backend/app/schemas/category.py
new file mode 100644
index 00000000..5705d219
--- /dev/null
+++ b/backend/app/schemas/category.py
@@ -0,0 +1,61 @@
+"""商品分类Schema"""
+from typing import Optional, List
+from pydantic import BaseModel, Field
+
+
+class CategoryBase(BaseModel):
+ """分类基础Schema"""
+ name: str = Field(..., description="分类名称")
+ image_url: Optional[str] = Field(None, description="分类图片URL")
+ parent_id: Optional[str] = Field(None, description="父分类ID")
+ sort_order: int = Field(0, description="排序")
+ is_active: bool = Field(True, description="是否启用")
+
+
+class CategoryCreate(CategoryBase):
+ """创建分类"""
+ pass
+
+
+class CategoryUpdate(BaseModel):
+ """更新分类"""
+ name: Optional[str] = Field(None, description="分类名称")
+ image_url: Optional[str] = Field(None, description="分类图片URL")
+ parent_id: Optional[str] = Field(None, description="父分类ID")
+ sort_order: Optional[int] = Field(None, description="排序")
+ is_active: Optional[bool] = Field(None, description="是否启用")
+
+
+class CategoryTreeNode(BaseModel):
+ """分类树节点"""
+ id: str
+ name: str
+ parentId: Optional[str] = None
+ sortOrder: int = 0
+ isActive: bool = True
+ children: List["CategoryTreeNode"] = []
+
+ class Config:
+ from_attributes = True
+
+
+class CategoryResponse(CategoryBase):
+ """分类响应"""
+ id: str
+ create_at: Optional[str] = None
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[str] = None
+ update_by: Optional[str] = None
+
+ class Config:
+ from_attributes = True
+
+
+class CategoryListResponse(BaseModel):
+ """分类列表响应"""
+ items: List[CategoryResponse]
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
diff --git a/backend/app/schemas/category_parameter.py b/backend/app/schemas/category_parameter.py
new file mode 100644
index 00000000..2ab48ac0
--- /dev/null
+++ b/backend/app/schemas/category_parameter.py
@@ -0,0 +1,82 @@
+"""Pydantic schemas - 分类参数关联"""
+from pydantic import BaseModel, Field, ConfigDict
+from typing import Optional, List
+from datetime import datetime
+
+
+class CategoryParameterBase(BaseModel):
+ """分类参数关联基础字段"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ category_id: str = Field(..., description="分类ID", alias="categoryId")
+ parameter_id: str = Field(..., description="参数ID", alias="parameterId")
+ is_required: int = Field(default=0, description="该分类下是否必填:0-否, 1-是", alias="isRequired")
+ default_value: Optional[str] = Field(None, max_length=500, description="默认值", alias="defaultValue")
+ sort_order: int = Field(default=0, description="排序", alias="sortOrder")
+
+
+class CategoryParameterItem(BaseModel):
+ """批量更新中的单个参数项(不包含categoryId)"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ parameter_id: str = Field(..., description="参数ID", alias="parameterId")
+ is_required: int = Field(default=0, description="该分类下是否必填:0-否, 1-是", alias="isRequired")
+ default_value: Optional[str] = Field(None, max_length=500, description="默认值", alias="defaultValue")
+ sort_order: int = Field(default=0, description="排序", alias="sortOrder")
+
+
+class CategoryParameterCreate(CategoryParameterBase):
+ """创建分类参数关联请求"""
+ pass
+
+
+class CategoryParameterUpdate(BaseModel):
+ """更新分类参数关联请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ is_required: Optional[int] = Field(None, description="是否必填", alias="isRequired")
+ default_value: Optional[str] = Field(None, max_length=500, description="默认值", alias="defaultValue")
+ sort_order: Optional[int] = Field(None, description="排序", alias="sortOrder")
+
+
+class CategoryParameterBatchUpdate(BaseModel):
+ """批量更新分类参数关联请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ items: List[CategoryParameterItem] = Field(..., description="参数关联列表")
+
+
+class CategoryParameterResponse(CategoryParameterBase):
+ """分类参数关联响应"""
+ id: str
+ create_at: datetime
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class CategoryParameterWithDetailResponse(BaseModel):
+ """带参数详情的分类参数关联响应"""
+ id: str
+ category_id: str
+ parameter_id: str
+ parameter_name: str
+ parameter_code: str
+ param_type: str
+ options: Optional[str] = None
+ placeholder: Optional[str] = None
+ is_required: int
+ default_value: Optional[str] = None
+ sort_order: int
+
+ class Config:
+ from_attributes = True
+
+
+class CategoryParameterListResponse(BaseModel):
+ """分类参数关联列表响应"""
+ list: List[CategoryParameterWithDetailResponse]
+ total: int
diff --git a/backend/app/schemas/common.py b/backend/app/schemas/common.py
new file mode 100644
index 00000000..1ef23871
--- /dev/null
+++ b/backend/app/schemas/common.py
@@ -0,0 +1,33 @@
+"""公共Schema定义"""
+from typing import Optional
+from pydantic import BaseModel
+
+
+class PaginationParams(BaseModel):
+ """分页参数"""
+ page: int = 1
+ pageSize: int = 10
+
+
+class PaginationResult(BaseModel):
+ """分页结果"""
+ items: list = []
+ total: int = 0
+ page: int = 1
+ pageSize: int = 10
+ totalPages: int = 0
+
+
+class IdInput(BaseModel):
+ """ID输入"""
+ id: str
+
+
+class StatusInput(BaseModel):
+ """状态输入"""
+ is_active: bool = True
+
+
+class DeleteInput(BaseModel):
+ """删除输入"""
+ pass
diff --git a/backend/app/schemas/delivery.py b/backend/app/schemas/delivery.py
new file mode 100644
index 00000000..4ab52760
--- /dev/null
+++ b/backend/app/schemas/delivery.py
@@ -0,0 +1,265 @@
+"""Pydantic schemas - 配送任务管理"""
+from pydantic import BaseModel, Field
+from typing import Optional, List
+from datetime import datetime
+from decimal import Decimal
+
+
+class DeliveryTaskItemResponse(BaseModel):
+ """配送任务明细响应"""
+ id: str
+ taskId: str
+ orderId: Optional[str] = None
+ orderItemId: Optional[str] = None
+ productId: Optional[str] = None
+ productName: Optional[str] = None
+ clientId: Optional[str] = None
+ deliveryAddress: Optional[str] = None
+ contactPerson: Optional[str] = None
+ contactPhone: Optional[str] = None
+ quantity: Decimal
+ deliveredQuantity: Optional[Decimal] = None
+ status: str
+ deliveryTime: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class DeliveryTaskResponse(BaseModel):
+ """配送任务响应"""
+ id: str
+ taskNo: str
+ orderId: Optional[str] = None
+ regionId: Optional[str] = None
+ regionName: Optional[str] = None
+ supplierId: Optional[str] = None
+ supplierName: Optional[str] = None
+ deliverId: Optional[str] = None
+ deliverName: Optional[str] = None
+ status: str
+ effectiveTime: Optional[datetime] = None
+ pickupTime: Optional[datetime] = None
+ pickupRemark: Optional[str] = None
+ pickupImageUrl: Optional[str] = None
+ deliveryTime: Optional[datetime] = None
+ deliveryRemark: Optional[str] = None
+ deliveryImageUrl: Optional[str] = None
+ createAt: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class DeliveryTaskDetailResponse(DeliveryTaskResponse):
+ """配送任务详情响应"""
+ items: List[DeliveryTaskItemResponse] = []
+ order: Optional[dict] = None
+ deliver: Optional[dict] = None
+
+
+class DeliveryTaskListResponse(BaseModel):
+ """配送任务列表响应"""
+ items: List[DeliveryTaskResponse]
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
+
+
+class DeliveryTaskCreateRequest(BaseModel):
+ """创建配送任务请求"""
+ orderId: str = Field(..., description="订单ID")
+ regionId: str = Field(..., description="区域ID")
+ supplierId: str = Field(..., description="供应商ID")
+ deliverId: Optional[str] = Field(None, description="配送员ID")
+ effectiveTime: Optional[datetime] = Field(None, description="配送截止时间")
+
+
+class DeliveryTaskAssignRequest(BaseModel):
+ """分配配送员请求"""
+ deliverId: str = Field(..., description="配送员ID")
+
+
+class DeliveryTaskPickupRequest(BaseModel):
+ """确认取货请求"""
+ pickupRemark: Optional[str] = Field(None, description="取货备注")
+ pickupImageUrl: Optional[str] = Field(None, description="取货图片URL")
+
+
+class DeliveryTaskDeliverRequest(BaseModel):
+ """确认送达请求"""
+ deliveryRemark: Optional[str] = Field(None, description="送达备注")
+ deliveryImageUrl: Optional[str] = Field(None, description="送达图片URL")
+
+
+class DeliveryTaskCancelRequest(BaseModel):
+ """取消配送任务请求"""
+ reason: str = Field(..., description="取消原因")
+
+
+class DeliveryTaskStatusUpdateRequest(BaseModel):
+ """更新配送任务状态请求"""
+ status: str = Field(..., description="新状态")
+ remark: Optional[str] = Field(None, description="备注")
+
+
+# ============ 供应商端配送任务 Schema ============
+
+
+class SupplierTaskResponse(BaseModel):
+ """供应商配送任务响应"""
+ id: str
+ taskNo: str
+ orderId: Optional[str] = None
+ regionId: Optional[str] = None
+ regionName: Optional[str] = None
+ clientName: Optional[str] = None
+ supplierId: Optional[str] = None
+ deliverId: Optional[str] = None
+ deliverName: Optional[str] = None
+ status: str
+ effectiveTime: Optional[datetime] = None
+ pickupTime: Optional[datetime] = None
+ pickupRemark: Optional[str] = None
+ deliveryTime: Optional[datetime] = None
+ totalItems: int = 0
+ deliveredItems: int = 0
+ createAt: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class SupplierTaskListResponse(BaseModel):
+ """供应商配送任务列表响应"""
+ items: List[SupplierTaskResponse]
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
+
+
+class SupplierConfirmPickupRequest(BaseModel):
+ """供应商确认取货请求"""
+ pickupRemark: Optional[str] = Field(None, description="取货备注")
+
+
+class SupplierConfirmPreparedRequest(BaseModel):
+ """供应商确认备货完成请求"""
+ preparedRemark: Optional[str] = Field(None, description="备货备注")
+
+
+# ============ 配送统计 Schema ============
+
+
+class DeliveryStatisticsResponse(BaseModel):
+ """配送统计响应"""
+ totalTasks: int
+ pendingTasks: int
+ inProgressTasks: int
+ completedTasks: int
+ cancelledTasks: int
+ averageDeliveryTime: Optional[float] = None
+ onTimeRate: Optional[float] = None
+
+
+class DeliveryWarningItem(BaseModel):
+ """配送预警项"""
+ taskId: str
+ taskNo: str
+ urgencyLevel: str
+ remainingMinutes: int
+ orderId: Optional[str] = None
+ clientName: Optional[str] = None
+ deliverName: Optional[str] = None
+
+
+class DeliveryWarningListResponse(BaseModel):
+ """配送预警列表响应"""
+ items: List[DeliveryWarningItem] = []
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
+
+
+# ============ 运营端配送任务 Schema (兼容旧命名) ============
+
+
+class DeliveryAssignRequest(BaseModel):
+ """分配配送员请求"""
+ deliverId: str = Field(..., description="配送员ID")
+
+
+class DeliveryAssignBatchRequest(BaseModel):
+ """批量分配配送员请求"""
+ taskIds: List[str] = Field(..., description="配送任务ID列表")
+ deliverId: str = Field(..., description="配送员ID")
+
+
+class DeliveryPickupRequest(BaseModel):
+ """配送员取货确认请求"""
+ taskId: str = Field(..., description="配送任务ID")
+ pickupRemark: Optional[str] = Field(None, description="取货备注")
+ pickupImageUrl: Optional[str] = Field(None, description="取货图片")
+
+
+class DeliveryDeliverItem(BaseModel):
+ """送达明细"""
+ taskItemId: str = Field(..., description="配送任务明细ID")
+ deliveredQuantity: Decimal = Field(..., description="送达数量")
+
+
+class DeliveryDeliverRequest(BaseModel):
+ """确认送达请求"""
+ taskId: str = Field(..., description="配送任务ID")
+ items: List[DeliveryDeliverItem] = Field(default_factory=list, description="送达明细列表")
+ deliveryRemark: Optional[str] = Field(None, description="送达备注")
+ deliveryImageUrl: Optional[str] = Field(None, description="送达图片")
+
+
+class DeliveryCancelRequest(BaseModel):
+ """取消配送任务请求"""
+ taskId: str = Field(..., description="配送任务ID")
+ reason: str = Field(..., description="取消原因")
+
+
+class DeliveryReassignRequest(BaseModel):
+ """重新分配配送员请求"""
+ taskId: str = Field(..., description="配送任务ID")
+ newDeliverId: str = Field(..., description="新配送员ID")
+ reason: Optional[str] = Field(None, description="重新分配原因")
+
+
+class DeliveryStatusResponse(BaseModel):
+ """配送状态更新响应"""
+ id: str
+ status: str
+ message: str = "操作成功"
+
+
+class DeliveryTaskStatistics(BaseModel):
+ """配送任务统计"""
+ total: int = 0
+ pending: int = 0
+ picking: int = 0
+ delivering: int = 0
+ completed: int = 0
+ cancelled: int = 0
+
+
+class DeliveryAttachmentResponse(BaseModel):
+ """配送附件响应"""
+ id: str
+ taskId: str
+ type: str
+ url: str
+ createAt: datetime
+
+ class Config:
+ from_attributes = True
+
+
+# 兼容别名
+DeliveryCompleteRequest = DeliveryTaskDeliverRequest
diff --git a/backend/app/schemas/inquiry.py b/backend/app/schemas/inquiry.py
new file mode 100644
index 00000000..83a1a3ba
--- /dev/null
+++ b/backend/app/schemas/inquiry.py
@@ -0,0 +1,81 @@
+"""Pydantic schemas - 询价单管理"""
+from pydantic import BaseModel, Field
+from typing import Optional, List
+from datetime import datetime
+from decimal import Decimal
+
+
+class InquiryItemBase(BaseModel):
+ """询价单项基础字段"""
+ product_id: str = Field(..., description="商品ID")
+
+
+class InquiryItemCreate(InquiryItemBase):
+ """创建询价单项请求"""
+ pass
+
+
+class InquiryItemResponse(InquiryItemBase):
+ """询价单项响应"""
+ id: str
+ inquiry_id: str
+ is_del: int
+
+ class Config:
+ from_attributes = True
+
+
+class InquiryBase(BaseModel):
+ """询价单基础字段"""
+ region_id: str = Field(..., description="区域ID")
+ title: str = Field(..., max_length=200, description="询价单标题")
+ remark: Optional[str] = Field(None, max_length=1000, description="备注")
+
+
+class InquiryCreate(InquiryBase):
+ """创建询价单请求"""
+ product_ids: List[str] = Field(default_factory=list, description="商品ID列表")
+
+
+class InquiryUpdate(BaseModel):
+ """更新询价单请求"""
+ title: Optional[str] = Field(None, max_length=200, description="询价单标题")
+ remark: Optional[str] = Field(None, max_length=1000, description="备注")
+ product_ids: Optional[List[str]] = Field(None, description="商品ID列表")
+
+
+class InquiryResponse(InquiryBase):
+ """询价单响应"""
+ id: str
+ status: str
+ publish_time: Optional[datetime] = None
+ close_time: Optional[datetime] = None
+ is_del: int
+ create_at: Optional[datetime] = None
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class InquiryDetailResponse(InquiryResponse):
+ """询价单详情响应(包含询价项)"""
+ items: List[InquiryItemResponse] = []
+
+
+class InquiryListResponse(BaseModel):
+ """询价单列表响应"""
+ items: List[InquiryResponse]
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
+
+
+class InquiryStatusResponse(BaseModel):
+ """询价单状态更新响应"""
+ id: str
+ status: str
+ message: str = "操作成功"
diff --git a/backend/app/schemas/menu.py b/backend/app/schemas/menu.py
new file mode 100644
index 00000000..672055ad
--- /dev/null
+++ b/backend/app/schemas/menu.py
@@ -0,0 +1,35 @@
+"""菜单 Schema 定义"""
+from typing import Optional, List
+from pydantic import BaseModel
+
+
+class MenuMeta(BaseModel):
+ """菜单元信息"""
+ title: str
+ icon: Optional[str] = None
+ keepAlive: Optional[bool] = False
+ isHide: Optional[bool] = False
+ isIframe: Optional[bool] = False
+ link: Optional[str] = None
+ showBadge: Optional[bool] = False
+ showTextBadge: Optional[str] = None
+ isHideTab: Optional[bool] = False
+
+
+class MenuItem(BaseModel):
+ """菜单项"""
+ id: int
+ name: str
+ path: str
+ component: Optional[str] = None
+ redirect: Optional[str] = None
+ meta: Optional[MenuMeta] = None
+ children: Optional[List["MenuItem"]] = None
+
+
+class MenuListResponse(BaseModel):
+ """菜单列表响应"""
+ menuList: List[MenuItem]
+
+
+MenuItem.model_rebuild()
diff --git a/backend/app/schemas/order.py b/backend/app/schemas/order.py
new file mode 100644
index 00000000..804e7759
--- /dev/null
+++ b/backend/app/schemas/order.py
@@ -0,0 +1,339 @@
+"""Pydantic schemas - 订单管理"""
+from pydantic import BaseModel, Field
+from typing import Optional, List
+from datetime import datetime
+from decimal import Decimal
+
+
+class OrderItemBase(BaseModel):
+ """订单项基础字段"""
+ productId: str = Field(..., description="商品ID")
+ unitId: str = Field(..., description="单位ID")
+ quantity: Decimal = Field(..., description="数量")
+ unitPrice: Decimal = Field(..., description="单价")
+
+
+class OrderItemCreate(OrderItemBase):
+ """创建订单项请求"""
+ note: Optional[str] = None
+
+
+class OrderItemResponse(BaseModel):
+ """订单项响应"""
+ id: str
+ productId: str
+ productName: Optional[str] = None
+ unitId: str
+ unitName: Optional[str] = None
+ quantity: Decimal
+ unitPrice: Decimal
+ amount: Decimal
+ deliveredQuantity: Optional[Decimal] = None
+ status: str
+
+ class Config:
+ from_attributes = True
+
+
+class OrderCreate(BaseModel):
+ """创建订单请求"""
+ priceListId: str = Field(..., description="价格清单ID")
+ items: List[OrderItemCreate] = Field(..., description="订单项列表")
+ addressId: str = Field(..., description="配送地址ID")
+ paymentMethod: str = Field(..., description="支付方式: wechat/offline")
+ remark: Optional[str] = Field(None, description="备注")
+
+
+class OrderPreviewRequest(BaseModel):
+ """订单预览请求"""
+ priceListId: str = Field(..., description="价格清单ID")
+ items: List[dict] = Field(..., description="商品列表")
+ addressId: str = Field(..., description="配送地址ID")
+
+
+class OrderResponse(BaseModel):
+ """订单响应"""
+ id: str
+ orderNo: str
+ regionId: str
+ regionName: Optional[str] = None
+ clientId: str
+ clientName: Optional[str] = None
+ priceListId: str
+ priceVersion: int
+ status: str
+ paymentMethod: Optional[str] = None
+ totalAmount: Decimal
+ paidAmount: Decimal
+ receivableAmount: Decimal
+ refundAmount: Decimal
+ contactPerson: Optional[str] = None
+ contactPhone: Optional[str] = None
+ deliveryAddress: Optional[str] = None
+ remark: Optional[str] = None
+ lockOperatorId: Optional[str] = None
+ lockTime: Optional[datetime] = None
+ createAt: Optional[datetime] = None
+ paymentTime: Optional[datetime] = None
+ cancelReason: Optional[str] = None
+ cancelReviewNote: Optional[str] = None
+ completedTime: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class OrderDetailResponse(OrderResponse):
+ """订单详情响应(包含订单项和状态日志)"""
+ items: List[OrderItemResponse] = []
+ statusLogs: List[dict] = []
+
+
+class OrderListResponse(BaseModel):
+ """订单列表响应"""
+ items: List[OrderResponse]
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
+
+
+class OrderPreviewResponse(BaseModel):
+ """订单预览响应"""
+ priceListId: str
+ priceVersion: int
+ items: List[dict]
+ totalAmount: Decimal
+ address: dict
+
+
+class OrderCancelRequest(BaseModel):
+ """取消订单请求"""
+ reason: str = Field(..., description="取消原因")
+
+
+class OrderDirectCancelRequest(BaseModel):
+ """直接取消订单请求(未支付订单)"""
+ reason: str = Field(..., description="取消原因")
+
+
+class OrderReviewCancelRequest(BaseModel):
+ """审核取消订单请求"""
+ approved: bool = Field(..., description="是否审核通过")
+ reviewNote: Optional[str] = Field(None, description="审核备注")
+
+
+class OrderStatusResponse(BaseModel):
+ """订单状态更新响应"""
+ id: str
+ status: str
+ message: str = "操作成功"
+
+
+class OrderStatusLogResponse(BaseModel):
+ """订单状态日志响应"""
+ id: str
+ fromStatus: Optional[str] = None
+ toStatus: str
+ toStatusText: str
+ operatorName: Optional[str] = None
+ operatorRole: str
+ reason: Optional[str] = None
+ note: Optional[str] = None
+ createAt: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class DeliveryProgressResponse(BaseModel):
+ """配送进度响应"""
+ orderId: str
+ orderNo: str
+ status: str
+ deliveryStatus: Optional[str] = None
+ items: List[dict] = []
+ estimatedDeliveryTime: Optional[str] = None
+
+
+# ============ 订单调整相关 Schema ============
+
+
+class OrderAdjustItem(BaseModel):
+ """订单调整项"""
+ orderItemId: str = Field(..., description="订单项ID")
+ adjustType: str = Field(..., description="调整类型: reduce/delete/substitute")
+ newQuantity: Optional[Decimal] = Field(None, description="新数量(减量时使用)")
+ newProductId: Optional[str] = Field(None, description="新商品ID(替代时使用)")
+ reason: str = Field(..., description="调整原因")
+
+
+class OrderAdjustRequest(BaseModel):
+ """订单调整请求"""
+ adjustments: List[OrderAdjustItem] = Field(..., description="调整项列表")
+
+
+class OrderAdjustResponse(BaseModel):
+ """订单调整响应"""
+ id: str
+ orderId: str
+ orderItemId: Optional[str] = None
+ adjustType: str
+ beforeQuantity: Decimal
+ afterQuantity: Decimal
+ beforeAmount: Decimal
+ afterAmount: Decimal
+ differenceAmount: Decimal
+ reason: str
+ operatorName: str
+ createAt: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class OrderAdjustResult(BaseModel):
+ """订单调整结果"""
+ id: str
+ totalAmount: Decimal
+ receivableAmount: Decimal
+ adjustments: List[dict]
+ message: str = "调整成功"
+
+
+# ============ 订单锁定相关 Schema ============
+
+
+class OrderLockRequest(BaseModel):
+ """订单锁定请求"""
+ pass
+
+
+class OrderLockResponse(BaseModel):
+ """订单锁定响应"""
+ id: str
+ lockOperatorId: Optional[str] = None
+ lockTime: Optional[datetime] = None
+ message: str = "锁定成功"
+
+
+class OrderUnlockRequest(BaseModel):
+ """订单解锁请求"""
+ pass
+
+
+class OrderUnlockResponse(BaseModel):
+ """订单解锁响应"""
+ id: str
+ message: str = "解锁成功"
+
+
+# ============ 订单状态管理 Schema ============
+
+
+class OrderForceCompleteRequest(BaseModel):
+ """强制完结订单请求"""
+ reason: Optional[str] = Field(None, description="完结原因")
+
+
+class OrderForceCompleteResponse(BaseModel):
+ """强制完结订单响应"""
+ id: str
+ status: str
+ completedTime: Optional[datetime] = None
+ message: str = "强制完结成功"
+
+
+class OrderPartialDeliveredRequest(BaseModel):
+ """标记部分送达请求"""
+ reason: Optional[str] = Field(None, description="原因")
+
+
+class OrderPartialDeliveredResponse(BaseModel):
+ """标记部分送达响应"""
+ id: str
+ status: str
+ message: str = "已标记为部分送达"
+
+
+class OrderConfirmPartialRequest(BaseModel):
+ """确认部分送达请求"""
+ reason: Optional[str] = Field(None, description="原因")
+
+
+class OrderConfirmPartialResponse(BaseModel):
+ """确认部分送达响应"""
+ id: str
+ status: str
+ receivableAmount: Decimal
+ message: str = "已确认"
+
+
+# ============ 配送任务分配 Schema ============
+
+
+class AssignDelivererRequest(BaseModel):
+ """分配配送员请求"""
+ deliverId: str = Field(..., description="配送员ID")
+
+
+class AssignDelivererResponse(BaseModel):
+ """分配配送员响应"""
+ id: str
+ taskNo: str
+ deliverId: str
+ deliverName: str
+ status: str
+ message: str = "配送员分配成功"
+
+
+# ============ 配送时效预警 Schema ============
+
+
+class DeliveryWarningResponse(BaseModel):
+ """配送时效预警响应"""
+ id: str
+ taskNo: str
+ orderId: Optional[str] = None
+ regionId: Optional[str] = None
+ regionName: Optional[str] = None
+ clientName: Optional[str] = None
+ supplierId: Optional[str] = None
+ supplierName: Optional[str] = None
+ deliverId: Optional[str] = None
+ deliverName: Optional[str] = None
+ status: str
+ effectiveTime: Optional[datetime] = None
+ remainingMinutes: int
+ urgencyLevel: str
+ isAssigned: bool
+ createAt: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class DeliveryWarningStatistics(BaseModel):
+ """配送时效预警统计"""
+ totalPending: int
+ criticalCount: int
+ urgentCount: int
+ normalCount: int
+ unassignedCount: int
+
+
+class ReassignDeliveryRequest(BaseModel):
+ """紧急重新分配配送任务请求"""
+ deliverId: str = Field(..., description="新配送员ID")
+ reason: Optional[str] = Field(None, description="重新分配原因")
+
+
+class ReassignDeliveryResponse(BaseModel):
+ """紧急重新分配配送任务响应"""
+ id: str
+ taskNo: str
+ newDeliverId: str
+ newDeliverName: str
+ oldDeliverId: Optional[str] = None
+ oldDeliverName: Optional[str] = None
+ message: str = "任务重新分配成功"
diff --git a/backend/app/schemas/order_adjust.py b/backend/app/schemas/order_adjust.py
new file mode 100644
index 00000000..396b904c
--- /dev/null
+++ b/backend/app/schemas/order_adjust.py
@@ -0,0 +1,17 @@
+"""订单调整相关枚举"""
+import enum
+
+
+class AdjustType(str, enum.Enum):
+ """调整类型"""
+ REDUCE = "reduce" # 减量
+ DELETE = "delete" # 删除
+ SUBSTITUTE = "substitute" # 替代
+
+
+class AdjustStatus(str, enum.Enum):
+ """调整状态"""
+ PENDING = "pending" # 待处理
+ APPROVED = "approved" # 已批准
+ REJECTED = "rejected" # 已拒绝
+ CANCELLED = "cancelled" # 已取消
diff --git a/backend/app/schemas/price_list.py b/backend/app/schemas/price_list.py
new file mode 100644
index 00000000..df4dcf8a
--- /dev/null
+++ b/backend/app/schemas/price_list.py
@@ -0,0 +1,160 @@
+"""Pydantic schemas - 价格清单管理"""
+from pydantic import BaseModel, Field
+from typing import Optional, List
+from datetime import datetime
+from decimal import Decimal
+
+
+class PriceListItemBase(BaseModel):
+ """价格清单项基础字段"""
+ product_id: str = Field(..., description="商品ID")
+ unit_id: str = Field(..., description="单位ID")
+ price: Decimal = Field(..., description="单价")
+
+
+class PriceListItemCreate(PriceListItemBase):
+ """创建价格清单项请求"""
+ pass
+
+
+class PriceListItemUpdate(BaseModel):
+ """更新价格清单项请求"""
+ unit_id: Optional[str] = None
+ price: Optional[Decimal] = None
+
+
+class PriceListItemResponse(PriceListItemBase):
+ """价格清单项响应"""
+ id: str
+ price_list_id: str
+ is_del: int
+ create_at: Optional[datetime] = None
+ # 关联信息
+ product_name: Optional[str] = None
+ category_name: Optional[str] = None
+ unit_name: Optional[str] = None
+ image_url: Optional[str] = None
+
+ class Config:
+ from_attributes = True
+
+
+class PriceListBase(BaseModel):
+ """价格清单基础字段"""
+ region_id: str = Field(..., description="区域ID")
+ supplier_id: str = Field(..., description="供应商ID")
+
+
+class PriceListCreate(PriceListBase):
+ """创建价格清单请求"""
+ effective_time: Optional[datetime] = Field(None, description="生效时间")
+ effective_type: str = Field("immediate", description="生效方式: immediate-立即生效/scheduled-定时生效")
+ items: List[PriceListItemCreate] = Field(default_factory=list, description="价格项列表")
+ remark: Optional[str] = Field(None, max_length=500, description="备注")
+
+
+class PriceListUpdate(BaseModel):
+ """更新价格清单请求"""
+ effective_time: Optional[datetime] = None
+ items: Optional[List[PriceListItemCreate]] = None
+ remark: Optional[str] = None
+
+
+class PriceListResponse(BaseModel):
+ """价格清单响应"""
+ id: str
+ region_id: str
+ supplier_id: str
+ version: int
+ status: str
+ effective_time: Optional[datetime] = None
+ published_by: Optional[str] = None
+ published_time: Optional[datetime] = None
+ remark: Optional[str] = None
+ is_del: int
+ create_at: Optional[datetime] = None
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class PriceListDetailResponse(PriceListResponse):
+ """价格清单详情响应(包含价格项)"""
+ items: List[PriceListItemResponse] = []
+ # 关联信息
+ region_name: Optional[str] = None
+ supplier_name: Optional[str] = None
+ published_by_name: Optional[str] = None
+
+
+class PriceListListResponse(BaseModel):
+ """价格清单列表响应"""
+ items: List[PriceListResponse]
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
+
+
+class PriceListPublishRequest(BaseModel):
+ """发布价格清单请求"""
+ effective_type: str = Field("immediate", description="生效方式: immediate-立即生效/scheduled-定时生效")
+ effective_time: Optional[datetime] = Field(None, description="定时生效时间")
+ remark: Optional[str] = Field(None, max_length=500, description="备注")
+
+
+class PriceListInvalidateRequest(BaseModel):
+ """使价格清单失效请求"""
+ remark: Optional[str] = Field(None, max_length=500, description="失效原因备注")
+
+
+class PriceListStatusResponse(BaseModel):
+ """价格清单状态更新响应"""
+ id: str
+ status: str
+ message: str = "操作成功"
+
+
+class ProductPriceResponse(BaseModel):
+ """商品价格响应"""
+ product_id: str
+ product_name: str
+ category_name: Optional[str] = None
+ unit_id: str
+ unit_name: Optional[str] = None
+ price: Decimal
+ price_list_id: str
+ price_list_version: int
+
+
+class ProductPriceHistoryItem(BaseModel):
+ """商品价格历史项"""
+ price: Decimal
+ unit_id: str
+ unit_name: Optional[str] = None
+ price_list_id: str
+ version: int
+ published_time: datetime
+ status: str
+
+
+class ProductPriceHistoryResponse(BaseModel):
+ """商品价格历史响应"""
+ product_id: str
+ product_name: str
+ history: List[ProductPriceHistoryItem]
+ total: int
+ page: int
+ pageSize: int
+
+
+class PriceListGenerateRequest(BaseModel):
+ """从报价单生成价格清单请求"""
+ quotation_id: str = Field(..., description="报价单ID")
+ region_id: str = Field(..., description="区域ID")
+ effective_type: str = Field("immediate", description="生效方式")
+ effective_time: Optional[datetime] = None
+ remark: Optional[str] = Field(None, max_length=500, description="备注")
diff --git a/backend/app/schemas/product.py b/backend/app/schemas/product.py
new file mode 100644
index 00000000..a481c272
--- /dev/null
+++ b/backend/app/schemas/product.py
@@ -0,0 +1,82 @@
+"""Pydantic schemas - 商品管理"""
+from pydantic import BaseModel, Field
+from typing import Optional
+from datetime import datetime
+
+
+class ProductUnitBase(BaseModel):
+ """商品单位基础字段"""
+ name: str = Field(..., max_length=50)
+ code: str = Field(..., max_length=20)
+
+
+class ProductUnitCreate(ProductUnitBase):
+ """创建商品单位请求"""
+ pass
+
+
+class ProductUnitResponse(ProductUnitBase):
+ """商品单位响应"""
+ id: int
+ is_active: bool
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+
+
+class ProductCategoryBase(BaseModel):
+ """商品分类基础字段"""
+ name: str = Field(..., max_length=100)
+ parent_id: Optional[int] = None
+ sort_order: int = 0
+
+
+class ProductCategoryCreate(ProductCategoryBase):
+ """创建商品分类请求"""
+ pass
+
+
+class ProductCategoryResponse(ProductCategoryBase):
+ """商品分类响应"""
+ id: int
+ is_active: bool
+ created_at: datetime
+
+ class Config:
+ from_attributes = True
+
+
+class ProductBase(BaseModel):
+ """商品基础字段"""
+ name: str = Field(..., max_length=200)
+ category_id: Optional[int] = None
+ unit_id: Optional[int] = None
+ description: Optional[str] = None
+ image_url: Optional[str] = None
+
+
+class ProductCreate(ProductBase):
+ """创建商品请求"""
+ pass
+
+
+class ProductUpdate(BaseModel):
+ """更新商品请求"""
+ name: Optional[str] = None
+ category_id: Optional[int] = None
+ unit_id: Optional[int] = None
+ description: Optional[str] = None
+ image_url: Optional[str] = None
+ is_active: Optional[bool] = None
+
+
+class ProductResponse(ProductBase):
+ """商品响应"""
+ id: int
+ is_active: bool
+ created_at: datetime
+ unit: Optional[ProductUnitResponse] = None
+
+ class Config:
+ from_attributes = True
diff --git a/backend/app/schemas/product_banner.py b/backend/app/schemas/product_banner.py
new file mode 100644
index 00000000..8ed5b3d2
--- /dev/null
+++ b/backend/app/schemas/product_banner.py
@@ -0,0 +1,69 @@
+"""Pydantic schemas - 商品轮播图"""
+from pydantic import BaseModel, Field, ConfigDict
+from typing import Optional, List
+from datetime import datetime
+
+
+class ProductBannerBase(BaseModel):
+ """商品轮播图基础字段"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ product_id: str = Field(..., description="商品ID", alias="productId")
+ media_type: str = Field(default="image", description="媒体类型:image-图片, video-视频", alias="mediaType")
+ file_url: str = Field(..., max_length=500, description="文件URL地址", alias="fileUrl")
+ thumbnail_url: Optional[str] = Field(None, max_length=500, description="缩略图URL", alias="thumbnailUrl")
+ sort_order: int = Field(default=0, description="排序", alias="sortOrder")
+
+
+class ProductBannerCreate(ProductBannerBase):
+ """创建商品轮播图请求"""
+ pass
+
+
+class ProductBannerCreateBatch(BaseModel):
+ """批量创建商品轮播图请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ items: List[ProductBannerCreate] = Field(..., description="轮播图列表")
+
+
+class ProductBannerUpdate(BaseModel):
+ """更新商品轮播图请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ media_type: Optional[str] = Field(None, alias="mediaType")
+ file_url: Optional[str] = Field(None, max_length=500, alias="fileUrl")
+ thumbnail_url: Optional[str] = Field(None, max_length=500, alias="thumbnailUrl")
+ sort_order: Optional[int] = Field(None, alias="sortOrder")
+
+
+class ProductBannerSortItem(BaseModel):
+ """单条排序项"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ id: str
+ sort_order: int = Field(..., alias="sortOrder")
+
+
+class ProductBannerSortUpdate(BaseModel):
+ """更新轮播图排序请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ items: List[ProductBannerSortItem] = Field(..., description="排序项列表")
+
+
+class ProductBannerResponse(ProductBannerBase):
+ """商品轮播图响应"""
+ model_config = ConfigDict(from_attributes=True, populate_by_name=True)
+
+ id: str
+ create_at: datetime
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[datetime] = None
+
+
+class ProductBannerListResponse(BaseModel):
+ """商品轮播图列表响应"""
+ list: List[ProductBannerResponse]
+ total: int
diff --git a/backend/app/schemas/product_detail_media.py b/backend/app/schemas/product_detail_media.py
new file mode 100644
index 00000000..777d34de
--- /dev/null
+++ b/backend/app/schemas/product_detail_media.py
@@ -0,0 +1,77 @@
+"""Pydantic schemas - 商品详情媒体"""
+from pydantic import BaseModel, Field, ConfigDict
+from typing import Optional, List
+from datetime import datetime
+
+
+class ProductDetailMediaBase(BaseModel):
+ """商品详情媒体基础字段"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ product_id: str = Field(..., description="商品ID", alias="productId")
+ content_type: str = Field(
+ default="image",
+ description="内容类型:image-图片, video-视频, text-文本",
+ alias="contentType",
+ )
+ content_text: Optional[str] = Field(None, description="文本内容", alias="contentText")
+ file_url: Optional[str] = Field(None, max_length=500, description="文件URL地址", alias="fileUrl")
+ thumbnail_url: Optional[str] = Field(None, max_length=500, description="缩略图URL", alias="thumbnailUrl")
+ description: Optional[str] = Field(None, max_length=500, description="描述", alias="description")
+ sort_order: int = Field(default=0, description="排序", alias="sortOrder")
+
+
+class ProductDetailMediaCreate(ProductDetailMediaBase):
+ """创建商品详情媒体请求"""
+ pass
+
+
+class ProductDetailMediaCreateBatch(BaseModel):
+ """批量创建商品详情媒体请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ items: List[ProductDetailMediaCreate] = Field(..., description="详情媒体列表")
+
+
+class ProductDetailMediaUpdate(BaseModel):
+ """更新商品详情媒体请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ content_type: Optional[str] = Field(None, alias="contentType")
+ content_text: Optional[str] = Field(None, alias="contentText")
+ file_url: Optional[str] = Field(None, alias="fileUrl")
+ thumbnail_url: Optional[str] = Field(None, alias="thumbnailUrl")
+ description: Optional[str] = Field(None, alias="description")
+ sort_order: Optional[int] = Field(None, alias="sortOrder")
+
+
+class ProductDetailMediaSortItem(BaseModel):
+ """单条排序项"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ id: str
+ sort_order: int = Field(..., alias="sortOrder")
+
+
+class ProductDetailMediaSortUpdate(BaseModel):
+ """更新详情媒体排序请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ items: List[ProductDetailMediaSortItem] = Field(..., description="排序项列表")
+
+
+class ProductDetailMediaResponse(ProductDetailMediaBase):
+ """商品详情媒体响应"""
+ model_config = ConfigDict(from_attributes=True, populate_by_name=True)
+
+ id: str
+ create_at: datetime
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[datetime] = None
+
+
+class ProductDetailMediaListResponse(BaseModel):
+ """商品详情媒体列表响应"""
+ list: List[ProductDetailMediaResponse]
+ total: int
diff --git a/backend/app/schemas/product_parameter.py b/backend/app/schemas/product_parameter.py
new file mode 100644
index 00000000..564ed2b9
--- /dev/null
+++ b/backend/app/schemas/product_parameter.py
@@ -0,0 +1,71 @@
+"""Pydantic schemas - 商品参数模板"""
+from pydantic import BaseModel, Field, ConfigDict
+from typing import Optional, List
+from datetime import datetime
+
+
+class ProductParameterBase(BaseModel):
+ """商品参数基础字段"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ name: str = Field(..., max_length=100, description="参数名称", alias="name")
+ code: str = Field(..., max_length=50, description="参数编码", alias="code")
+ param_type: str = Field(default="text", description="参数类型:text-文本输入, select-单选下拉, multi_select-多选", alias="paramType")
+ options: Optional[str] = Field(None, description="可选值列表,JSON数组格式", alias="options")
+ placeholder: Optional[str] = Field(None, max_length=100, description="输入提示文字", alias="placeholder")
+ is_required: int = Field(default=0, description="是否必填:0-否, 1-是", alias="isRequired")
+ is_active: int = Field(default=1, description="是否启用:0-禁用, 1-启用", alias="isActive")
+ sort_order: int = Field(default=0, description="排序", alias="sortOrder")
+ remark: Optional[str] = Field(None, max_length=500, description="备注说明", alias="remark")
+
+
+class ProductParameterCreate(ProductParameterBase):
+ """创建商品参数请求"""
+ pass
+
+
+class ProductParameterUpdate(BaseModel):
+ """更新商品参数请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ name: Optional[str] = Field(None, max_length=100, description="参数名称", alias="name")
+ code: Optional[str] = Field(None, max_length=50, description="参数编码", alias="code")
+ param_type: Optional[str] = Field(None, description="参数类型", alias="paramType")
+ options: Optional[str] = Field(None, description="可选值列表", alias="options")
+ placeholder: Optional[str] = Field(None, max_length=100, description="输入提示文字", alias="placeholder")
+ is_required: Optional[int] = Field(None, description="是否必填", alias="isRequired")
+ is_active: Optional[int] = Field(None, description="是否启用", alias="isActive")
+ sort_order: Optional[int] = Field(None, description="排序", alias="sortOrder")
+ remark: Optional[str] = Field(None, max_length=500, description="备注说明", alias="remark")
+
+
+class ProductParameterResponse(ProductParameterBase):
+ """商品参数响应"""
+ id: str
+ create_at: datetime
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class ProductParameterListResponse(BaseModel):
+ """商品参数列表响应"""
+ list: List[ProductParameterResponse]
+ total: int
+ page: int
+ pageSize: int
+
+
+class ProductParameterSimpleResponse(BaseModel):
+ """商品参数简洁响应(用于下拉选择)"""
+ id: str
+ name: str
+ code: str
+ param_type: str
+ options: Optional[str] = None
+
+ class Config:
+ from_attributes = True
diff --git a/backend/app/schemas/product_parameter_value.py b/backend/app/schemas/product_parameter_value.py
new file mode 100644
index 00000000..da743546
--- /dev/null
+++ b/backend/app/schemas/product_parameter_value.py
@@ -0,0 +1,75 @@
+"""Pydantic schemas - 商品参数值"""
+from pydantic import BaseModel, Field, ConfigDict
+from typing import Optional, List
+from datetime import datetime
+
+
+class ProductParameterValueBase(BaseModel):
+ """商品参数值基础字段"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ product_id: str = Field(..., description="商品ID", alias="productId")
+ parameter_id: str = Field(..., description="参数ID", alias="parameterId")
+ param_value: Optional[str] = Field(None, description="参数值", alias="paramValue")
+
+
+class ProductParameterValueCreate(ProductParameterValueBase):
+ """创建商品参数值请求"""
+ pass
+
+
+class ProductParameterValueUpdate(BaseModel):
+ """更新商品参数值请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ param_value: Optional[str] = Field(None, description="参数值", alias="paramValue")
+
+
+class ProductParameterValueBatchSave(BaseModel):
+ """批量保存商品参数值请求"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ product_id: str = Field(..., description="商品ID", alias="productId")
+ values: List[ProductParameterValueCreate] = Field(..., description="参数值列表")
+
+
+class ProductParameterValueResponse(BaseModel):
+ """商品参数值响应"""
+ id: str
+ product_id: str
+ parameter_id: str
+ parameter_name: Optional[str] = None
+ parameter_code: Optional[str] = None
+ param_type: Optional[str] = None
+ param_value: Optional[str] = None
+ sort_order: int
+ create_at: datetime
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class ProductParameterValueListResponse(BaseModel):
+ """商品参数值列表响应"""
+ list: List[ProductParameterValueResponse]
+ total: int
+
+
+class ProductParameterValueWithParameterResponse(BaseModel):
+ """带参数信息的商品参数值响应"""
+ id: str
+ product_id: str
+ parameter_id: str
+ parameter_name: str
+ parameter_code: str
+ param_type: str
+ options: Optional[str] = None
+ placeholder: Optional[str] = None
+ param_value: Optional[str] = None
+ sort_order: int
+
+ class Config:
+ from_attributes = True
diff --git a/backend/app/schemas/product_review.py b/backend/app/schemas/product_review.py
new file mode 100644
index 00000000..19ab748c
--- /dev/null
+++ b/backend/app/schemas/product_review.py
@@ -0,0 +1,110 @@
+"""Pydantic schemas - 售后评价"""
+from pydantic import BaseModel, Field
+from typing import Optional, List
+from datetime import datetime
+
+
+class ProductReviewBase(BaseModel):
+ """售后评价基础字段"""
+ product_id: str = Field(..., description="商品ID")
+ order_id: Optional[str] = Field(None, description="订单ID")
+ order_item_id: Optional[str] = Field(None, description="订单项ID")
+ user_id: str = Field(..., description="评价用户ID")
+ rating: int = Field(default=5, ge=1, le=5, description="评分(1-5星)")
+ rating_tags: Optional[str] = Field(None, max_length=500, description="评价标签,多个逗号分隔")
+ content: Optional[str] = Field(None, description="评价内容")
+ is_anonymous: int = Field(default=0, description="是否匿名:0-否, 1-是")
+
+
+class ProductReviewCreate(ProductReviewBase):
+ """创建售后评价请求"""
+ pass
+
+
+class ProductReviewUpdate(BaseModel):
+ """更新售后评价请求(用户可更新自己的评价)"""
+ rating: Optional[int] = Field(None, ge=1, le=5, description="评分")
+ rating_tags: Optional[str] = None
+ content: Optional[str] = None
+ is_anonymous: Optional[int] = None
+
+
+class ProductReviewReplyRequest(BaseModel):
+ """回复评价请求"""
+ reply_content: str = Field(..., description="回复内容")
+
+
+class ProductReviewRejectRequest(BaseModel):
+ """拒绝评价请求"""
+ reject_reason: str = Field(..., max_length=500, description="拒绝原因")
+
+
+class ProductReviewTopRequest(BaseModel):
+ """置顶评价请求"""
+ is_top: int = Field(..., description="是否置顶:0-否, 1-是")
+
+
+class ProductReviewResponse(ProductReviewBase):
+ """售后评价响应"""
+ id: str
+ user_name: Optional[str] = None
+ reply_content: Optional[str] = None
+ reply_time: Optional[datetime] = None
+ reply_by: Optional[str] = None
+ reply_by_name: Optional[str] = None
+ like_count: int = 0
+ is_top: int = 0
+ status: str = "pending"
+ reject_reason: Optional[str] = None
+ create_at: datetime
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class ProductReviewDetailResponse(ProductReviewResponse):
+ """售后评价详情响应(包含媒体列表)"""
+ media_list: List["ProductReviewMediaResponse"] = []
+
+
+class ProductReviewListResponse(BaseModel):
+ """售后评价列表响应"""
+ list: List[ProductReviewDetailResponse]
+ total: int
+ page: int
+ pageSize: int
+
+
+class ProductReviewStatisticsResponse(BaseModel):
+ """评价统计响应"""
+ total_count: int = Field(description="总评价数")
+ pending_count: int = Field(description="待审核数")
+ approved_count: int = Field(description="已通过数")
+ rejected_count: int = Field(description="已拒绝数")
+ avg_rating: float = Field(description="平均评分")
+ rating_distribution: dict = Field(description="评分分布,格式:{'5': 10, '4': 5, '3': 2, '2': 1, '1': 0}")
+
+
+class ProductReviewAdminListRequest(BaseModel):
+ """管理员评价列表查询参数"""
+ product_id: Optional[str] = None
+ product_name: Optional[str] = None
+ user_name: Optional[str] = None
+ status: Optional[str] = None
+ rating: Optional[int] = None
+ start_date: Optional[str] = None
+ end_date: Optional[str] = None
+
+
+class MiniReviewListRequest(BaseModel):
+ """小程序评价列表查询参数"""
+ product_id: str = Field(..., description="商品ID")
+ has_media: Optional[int] = Field(None, description="是否有媒体:0-全部, 1-有图/视频")
+
+
+# 前向引用
+from app.schemas.product_review_media import ProductReviewMediaResponse
+ProductReviewDetailResponse.model_rebuild()
diff --git a/backend/app/schemas/product_review_media.py b/backend/app/schemas/product_review_media.py
new file mode 100644
index 00000000..f4c0bb6a
--- /dev/null
+++ b/backend/app/schemas/product_review_media.py
@@ -0,0 +1,49 @@
+"""Pydantic schemas - 评价媒体"""
+from pydantic import BaseModel, Field
+from typing import Optional, List
+from datetime import datetime
+
+
+class ProductReviewMediaBase(BaseModel):
+ """评价媒体基础字段"""
+ review_id: str = Field(..., description="评价ID")
+ media_type: str = Field(default="image", description="媒体类型:image-图片, video-视频")
+ file_url: str = Field(..., max_length=500, description="文件URL地址")
+ thumbnail_url: Optional[str] = Field(None, max_length=500, description="缩略图URL(视频时使用)")
+ sort_order: int = Field(default=0, description="排序(数字越小越靠前)")
+
+
+class ProductReviewMediaCreate(ProductReviewMediaBase):
+ """创建评价媒体请求"""
+ pass
+
+
+class ProductReviewMediaCreateBatch(BaseModel):
+ """批量创建评价媒体请求"""
+ items: List[ProductReviewMediaCreate] = Field(..., description="媒体列表")
+
+
+class ProductReviewMediaUpdate(BaseModel):
+ """更新评价媒体请求"""
+ media_type: Optional[str] = None
+ file_url: Optional[str] = None
+ thumbnail_url: Optional[str] = None
+ sort_order: Optional[int] = None
+
+
+class ProductReviewMediaResponse(ProductReviewMediaBase):
+ """评价媒体响应"""
+ id: str
+ create_at: datetime
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class ProductReviewMediaListResponse(BaseModel):
+ """评价媒体列表响应"""
+ list: List[ProductReviewMediaResponse]
+ total: int
diff --git a/backend/app/schemas/quotation.py b/backend/app/schemas/quotation.py
new file mode 100644
index 00000000..94156269
--- /dev/null
+++ b/backend/app/schemas/quotation.py
@@ -0,0 +1,106 @@
+"""Pydantic schemas - 报价单管理"""
+from pydantic import BaseModel, Field
+from typing import Optional, List
+from datetime import datetime
+from decimal import Decimal
+
+
+class QuotationItemBase(BaseModel):
+ """报价单项基础字段"""
+ product_id: str = Field(..., description="商品ID")
+ unit_id: str = Field(..., description="单位ID")
+ price: Decimal = Field(..., description="单价")
+
+
+class QuotationItemCreate(QuotationItemBase):
+ """创建报价单项请求"""
+ pass
+
+
+class QuotationItemUpdate(BaseModel):
+ """更新报价单项请求"""
+ unit_id: Optional[str] = None
+ price: Optional[Decimal] = None
+
+
+class QuotationItemResponse(QuotationItemBase):
+ """报价单项响应"""
+ id: str
+ quotation_id: str
+ is_del: int
+ create_at: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class QuotationBase(BaseModel):
+ """报价单基础字段"""
+ inquiry_id: str = Field(..., description="询价单ID")
+ items: List[QuotationItemCreate] = Field(default_factory=list, description="报价项列表")
+
+
+class QuotationCreate(QuotationBase):
+ """创建报价单请求"""
+ pass
+
+
+class QuotationUpdate(BaseModel):
+ """更新报价单请求"""
+ items: Optional[List[QuotationItemCreate]] = None
+
+
+class QuotationResponse(BaseModel):
+ """报价单响应"""
+ id: str
+ inquiry_id: str
+ supplier_id: str
+ region_id: str
+ status: str
+ total_amount: Decimal
+ submit_time: Optional[datetime] = None
+ is_del: int
+ create_at: Optional[datetime] = None
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[datetime] = None
+
+ class Config:
+ from_attributes = True
+
+
+class QuotationDetailResponse(QuotationResponse):
+ """报价单详情响应(包含报价项)"""
+ items: List[QuotationItemResponse] = []
+
+
+class QuotationListResponse(BaseModel):
+ """报价单列表响应"""
+ items: List[QuotationResponse]
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
+
+
+class QuotationStatusResponse(BaseModel):
+ """报价单状态更新响应"""
+ id: str
+ status: str
+ message: str = "操作成功"
+
+
+class QuotationWithSupplierResponse(QuotationResponse):
+ """带供应商信息的报价单响应"""
+ supplier_name: Optional[str] = None
+ supplier_phone: Optional[str] = None
+ region_name: Optional[str] = None
+
+
+class QuotationWithItemsResponse(QuotationDetailResponse):
+ """带询价信息的报价单响应"""
+ inquiry_title: Optional[str] = None
+ inquiry_status: Optional[str] = None
+ supplier_name: Optional[str] = None
+ supplier_phone: Optional[str] = None
+ region_name: Optional[str] = None
diff --git a/backend/app/schemas/region.py b/backend/app/schemas/region.py
new file mode 100644
index 00000000..ce9b7350
--- /dev/null
+++ b/backend/app/schemas/region.py
@@ -0,0 +1,67 @@
+"""区域Schema"""
+from typing import Optional, List
+from pydantic import BaseModel, Field, ConfigDict
+
+
+class RegionBase(BaseModel):
+ """区域基础Schema"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ name: str = Field(..., description="区域名称")
+ code: str = Field(..., description="区域编码")
+ description: Optional[str] = Field(None, description="区域描述")
+ sort_order: int = Field(0, alias="sortOrder", description="排序")
+ is_active: bool = Field(True, alias="isActive", description="是否启用")
+ expected_delivery_minutes: Optional[int] = Field(
+ None, alias="expectedDeliveryMinutes", description="预计配送时长(分钟)"
+ )
+ delivery_warn_advance_minutes: Optional[int] = Field(
+ None,
+ alias="deliveryWarnAdvanceMinutes",
+ description="计划到货前未送达时预警提前时间(分钟)",
+ )
+
+
+class RegionCreate(RegionBase):
+ """创建区域"""
+ pass
+
+
+class RegionUpdate(BaseModel):
+ """更新区域"""
+ model_config = ConfigDict(populate_by_name=True)
+
+ name: Optional[str] = Field(None, description="区域名称")
+ code: Optional[str] = Field(None, description="区域编码")
+ description: Optional[str] = Field(None, description="区域描述")
+ sort_order: Optional[int] = Field(None, alias="sortOrder", description="排序")
+ is_active: Optional[bool] = Field(None, alias="isActive", description="是否启用")
+ expected_delivery_minutes: Optional[int] = Field(
+ None, alias="expectedDeliveryMinutes", description="预计配送时长(分钟)"
+ )
+ delivery_warn_advance_minutes: Optional[int] = Field(
+ None,
+ alias="deliveryWarnAdvanceMinutes",
+ description="计划到货前未送达时预警提前时间(分钟)",
+ )
+
+
+class RegionResponse(RegionBase):
+ """区域响应"""
+ model_config = ConfigDict(from_attributes=True, populate_by_name=True)
+
+ id: str
+ create_at: Optional[str] = None
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[str] = None
+ update_by: Optional[str] = None
+
+
+class RegionListResponse(BaseModel):
+ """区域列表响应"""
+ items: List[RegionResponse]
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
diff --git a/backend/app/schemas/unit.py b/backend/app/schemas/unit.py
new file mode 100644
index 00000000..03bc4c30
--- /dev/null
+++ b/backend/app/schemas/unit.py
@@ -0,0 +1,46 @@
+"""商品单位Schema"""
+from typing import Optional, List
+from pydantic import BaseModel, Field
+
+
+class UnitBase(BaseModel):
+ """单位基础Schema"""
+ name: str = Field(..., description="单位名称")
+ code: str = Field(..., description="单位编码")
+ is_active: bool = Field(True, description="是否启用")
+ sort_order: int = Field(0, description="排序字段,数值越小越靠前")
+
+
+class UnitCreate(UnitBase):
+ """创建单位"""
+ pass
+
+
+class UnitUpdate(BaseModel):
+ """更新单位"""
+ name: Optional[str] = Field(None, description="单位名称")
+ code: Optional[str] = Field(None, description="单位编码")
+ is_active: Optional[bool] = Field(None, description="是否启用")
+ sort_order: Optional[int] = Field(None, description="排序字段")
+
+
+class UnitResponse(UnitBase):
+ """单位响应"""
+ id: str
+ create_at: Optional[str] = None
+ create_by: Optional[str] = None
+ create_name: Optional[str] = None
+ update_at: Optional[str] = None
+ update_by: Optional[str] = None
+
+ class Config:
+ from_attributes = True
+
+
+class UnitListResponse(BaseModel):
+ """单位列表响应"""
+ items: List[UnitResponse]
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py
new file mode 100644
index 00000000..ac82248a
--- /dev/null
+++ b/backend/app/schemas/user.py
@@ -0,0 +1,70 @@
+"""用户Schema"""
+from typing import Optional, List
+from pydantic import BaseModel, Field
+
+
+class UserBase(BaseModel):
+ """用户基础Schema"""
+ username: str = Field(..., description="用户名")
+ real_name: Optional[str] = Field(None, description="真实姓名")
+ phone: Optional[str] = Field(None, description="联系电话")
+ role: str = Field(..., description="角色")
+ region_id: Optional[str] = Field(None, description="所属区域ID")
+ is_active: bool = Field(True, description="是否激活")
+
+
+class UserCreate(UserBase):
+ """创建用户"""
+ pass
+
+
+class UserUpdate(BaseModel):
+ """更新用户"""
+ real_name: Optional[str] = Field(None, description="真实姓名")
+ phone: Optional[str] = Field(None, description="联系电话")
+ role: Optional[str] = Field(None, description="角色")
+ region_id: Optional[str] = Field(None, description="所属区域ID")
+ is_active: Optional[bool] = Field(None, description="是否激活")
+
+
+class UserResponse(UserBase):
+ """用户响应"""
+ id: str
+ create_at: Optional[str] = None
+
+ class Config:
+ from_attributes = True
+
+
+class UserListResponse(BaseModel):
+ """用户列表响应"""
+ items: List[UserResponse]
+ total: int
+ page: int
+ pageSize: int
+ totalPages: int
+
+
+class LoginRequest(BaseModel):
+ """登录请求"""
+ username: str = Field(..., description="用户名")
+ password: str = Field(..., description="密码")
+
+
+class LoginResponse(BaseModel):
+ """登录响应"""
+ token: str
+ refreshToken: str
+ expiresIn: int
+ userInfo: dict
+
+
+class ChangePasswordRequest(BaseModel):
+ """修改密码请求"""
+ oldPassword: str = Field(..., description="旧密码")
+ newPassword: str = Field(..., description="新密码")
+
+
+class ResetPasswordRequest(BaseModel):
+ """重置密码请求"""
+ userId: str = Field(..., description="用户ID")
diff --git a/backend/app/services/china_region_tree.py b/backend/app/services/china_region_tree.py
new file mode 100644
index 00000000..4f98a98b
--- /dev/null
+++ b/backend/app/services/china_region_tree.py
@@ -0,0 +1,70 @@
+"""中国行政区划(bas_china_region)树形结构,供运营端与小程序共用。"""
+from __future__ import annotations
+
+import copy
+import threading
+import time
+from collections import defaultdict
+from typing import Dict, List
+
+from sqlalchemy.orm import Session
+
+from app.models.models import BasChinaRegion
+
+_CHINA_REGION_TREE_CACHE_TTL_SEC = 600
+_china_region_tree_cache = {"expires_at": 0.0, "tree": None}
+_china_region_tree_cache_lock = threading.Lock()
+
+
+def query_china_regions_active(db: Session) -> List[BasChinaRegion]:
+ return (
+ db.query(
+ BasChinaRegion.id,
+ BasChinaRegion.parent_id,
+ BasChinaRegion.region_code,
+ BasChinaRegion.region_name,
+ BasChinaRegion.sort_order,
+ )
+ .filter(BasChinaRegion.is_del == 0)
+ .order_by(BasChinaRegion.sort_order.asc(), BasChinaRegion.region_code.asc())
+ .all()
+ )
+
+
+def build_china_region_tree(rows: List[BasChinaRegion]) -> List[dict]:
+ """{ value: region_code, label: region_name, children?: [...] },与历史 china-region-cascader.json 结构一致。"""
+ children_map: Dict[str, List[BasChinaRegion]] = defaultdict(list)
+ for r in rows:
+ key = getattr(r, "parent_id", None) or ""
+ children_map[key].append(r)
+ for key in children_map:
+ children_map[key].sort(key=lambda x: (getattr(x, "sort_order", 0), getattr(x, "region_code", "")))
+
+ def to_node(r: BasChinaRegion) -> dict:
+ rid = getattr(r, "id", None)
+ subs = children_map.get(rid, [])
+ node: dict = {"value": getattr(r, "region_code", None), "label": getattr(r, "region_name", None)}
+ if subs:
+ node["children"] = [to_node(c) for c in subs]
+ return node
+
+ roots = children_map.get("", [])
+ return [to_node(r) for r in roots]
+
+
+def get_china_region_tree_cached(db: Session) -> List[dict]:
+ """获取中国行政区划树,带进程内短 TTL 缓存。"""
+ now = time.time()
+ with _china_region_tree_cache_lock:
+ cache_tree = _china_region_tree_cache.get("tree")
+ expires_at = float(_china_region_tree_cache.get("expires_at") or 0.0)
+ if cache_tree is not None and now < expires_at:
+ return copy.deepcopy(cache_tree)
+
+ rows = query_china_regions_active(db)
+ tree = build_china_region_tree(rows)
+
+ with _china_region_tree_cache_lock:
+ _china_region_tree_cache["tree"] = tree
+ _china_region_tree_cache["expires_at"] = now + _CHINA_REGION_TREE_CACHE_TTL_SEC
+ return copy.deepcopy(tree)
diff --git a/backend/app/tasks/__init__.py b/backend/app/tasks/__init__.py
new file mode 100644
index 00000000..a81e5ae2
--- /dev/null
+++ b/backend/app/tasks/__init__.py
@@ -0,0 +1,20 @@
+"""定时任务模块(骨架:不启动叁强遗留的订单/配送定时任务)"""
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def init_scheduled_tasks(get_db_session):
+ """中科兴皖骨架工程 — 不注册 APScheduler 业务任务。"""
+ logger.info("init_scheduled_tasks: 已跳过业务定时任务(骨架模式)")
+
+
+def shutdown_scheduler():
+ """无调度器实例时为空操作。"""
+ pass
+
+
+__all__ = [
+ "init_scheduled_tasks",
+ "shutdown_scheduler",
+]
diff --git a/backend/app/tasks/delivery_warning.py b/backend/app/tasks/delivery_warning.py
new file mode 100644
index 00000000..e4f7125d
--- /dev/null
+++ b/backend/app/tasks/delivery_warning.py
@@ -0,0 +1,236 @@
+"""配送时效预警定时任务
+
+依据订单分配时写入的配送截止时间(biz_delivery_task.effective_time)及区域表 sys_region.delivery_warn_advance_minutes(缺省 30 分钟)生成运营端预警:
+
+- 进入「截止时间前 N 分钟」窗口且仍未完成配送:生成一条 advance_deadline 预警;
+- 已超过截止时间仍未完成:生成 timeout 预警;
+- 配送员确认送达(任务状态 delivering)或任务取消/完成时,由业务侧自动结案预警。
+
+建议:每 1~5 分钟执行一次 check_delivery_warnings(db)。
+"""
+import logging
+from datetime import datetime
+from typing import Optional
+
+from sqlalchemy.orm import Session
+
+from app.models.models import BizDeliveryTask, BizDeliveryWarning, SysRegion
+from app.core.utils import generate_id
+from app.crud.delivery_warning_resolve import auto_resolve_open_warnings_for_task
+
+logger = logging.getLogger(__name__)
+
+DEFAULT_ADVANCE_MINUTES = 30
+
+
+class DeliveryWarningTask:
+ """配送时效预警任务"""
+
+ def __init__(self, db: Session):
+ self.db = db
+
+ def check_delivery_warnings(self) -> dict:
+ """检查配送任务并生成预警"""
+ result = {
+ "checked": 0,
+ "warnings_created": 0,
+ "notifications_sent": 0,
+ "errors": []
+ }
+
+ try:
+ tasks = (
+ self.db.query(BizDeliveryTask)
+ .filter(BizDeliveryTask.is_del == 0)
+ .all()
+ )
+ result["checked"] = len(tasks)
+
+ for task in tasks:
+ try:
+ if task.status in ("delivering", "completed", "cancelled"):
+ auto_resolve_open_warnings_for_task(self.db, task.id)
+ continue
+
+ if task.status not in ("pending", "assigned", "picking"):
+ continue
+
+ warnings = self._check_task_warnings(task)
+ result["warnings_created"] += len(warnings)
+
+ for warning in warnings:
+ self._send_warning_notification(warning)
+ result["notifications_sent"] += 1
+
+ except Exception as e:
+ logger.error(f"处理任务 {task.id} 时出错: {str(e)}")
+ result["errors"].append({
+ "task_id": task.id,
+ "error": str(e)
+ })
+
+ self.db.commit()
+
+ except Exception as e:
+ logger.error(f"配送时效预警任务执行失败: {str(e)}")
+ self.db.rollback()
+ result["errors"].append({"task_id": "all", "error": str(e)})
+
+ return result
+
+ def _region_advance_minutes(self, region_id: str) -> int:
+ reg = self.db.query(SysRegion).filter(SysRegion.id == region_id, SysRegion.is_del == 0).first()
+ if not reg:
+ return DEFAULT_ADVANCE_MINUTES
+ m = getattr(reg, "delivery_warn_advance_minutes", None)
+ if m is None or int(m) < 1:
+ return DEFAULT_ADVANCE_MINUTES
+ return int(m)
+
+ def _check_task_warnings(self, task: BizDeliveryTask) -> list:
+ warnings = []
+
+ if not task.effective_time:
+ return warnings
+
+ now = datetime.now()
+ remaining_minutes = int((task.effective_time - now).total_seconds() // 60)
+ advance = self._region_advance_minutes(task.region_id)
+
+ warning_type = None
+ urgency_level = None
+ message = None
+ if remaining_minutes < 0:
+ warning_type = "timeout"
+ urgency_level = "已超时"
+ message = f"已超过配送时效 {abs(remaining_minutes)} 分钟,仍未完成配送"
+ elif remaining_minutes <= advance:
+ warning_type = "advance_deadline"
+ urgency_level = "时效预警"
+ message = f"距配送截止不足 {advance} 分钟(剩余约 {remaining_minutes} 分钟),请督促配送"
+ else:
+ return warnings
+
+ w = self._upsert_task_warning(
+ task=task,
+ warning_type=warning_type,
+ urgency_level=urgency_level,
+ remaining_minutes=remaining_minutes,
+ message=message,
+ )
+ if w:
+ warnings.append(w)
+
+ return warnings
+
+ def _upsert_task_warning(
+ self,
+ task: BizDeliveryTask,
+ warning_type: str,
+ urgency_level: str,
+ remaining_minutes: int,
+ message: str
+ ) -> Optional[BizDeliveryWarning]:
+ """单任务单预警:存在则更新,不存在则创建"""
+ try:
+ warning = self._get_latest_warning_by_task(task.id)
+ now = datetime.now()
+ if warning:
+ warning.warning_type = warning_type
+ warning.urgency_level = urgency_level
+ warning.remaining_minutes = remaining_minutes
+ warning.message = message
+ warning.order_id = task.order_id
+ warning.is_sent = 0
+ # 未完成前统一保持未结案(若历史上被手工结案,则重新激活)
+ warning.is_resolved = 0
+ warning.resolved_time = None
+ warning.resolved_by = None
+ warning.update_at = now
+ warning.update_by = "system"
+ self.db.flush()
+ return warning
+
+ warning = BizDeliveryWarning(
+ id=generate_id(),
+ task_id=task.id,
+ order_id=task.order_id,
+ warning_type=warning_type,
+ urgency_level=urgency_level,
+ remaining_minutes=remaining_minutes,
+ message=message,
+ is_sent=0,
+ is_resolved=0,
+ create_by="system",
+ create_name="系统",
+ )
+ self.db.add(warning)
+ self.db.flush()
+ return warning
+ except Exception as e:
+ logger.error(f"创建预警记录失败: {str(e)}")
+ return None
+
+ def _get_latest_warning_by_task(self, task_id: str) -> Optional[BizDeliveryWarning]:
+ return self.db.query(BizDeliveryWarning).filter(
+ BizDeliveryWarning.task_id == task_id,
+ BizDeliveryWarning.is_del == 0,
+ ).order_by(BizDeliveryWarning.update_at.desc(), BizDeliveryWarning.create_at.desc()).first()
+
+ def _send_warning_notification(self, warning: BizDeliveryWarning) -> bool:
+ """发送预警通知(可通过微信模板消息等)"""
+ try:
+ # 获取配送员信息
+ task = self.db.query(BizDeliveryTask).filter(
+ BizDeliveryTask.id == warning.task_id
+ ).first()
+
+ if not task or not task.deliver_id:
+ logger.warning(f"任务 {warning.task_id} 未分配配送员,跳过通知")
+ return False
+
+ # TODO: 调用微信模板消息接口发送通知
+ # from app.core.wechat_pay import send_delivery_warning_message
+ # send_delivery_warning_message(
+ # deliver_id=task.deliver_id,
+ # task_no=task.task_no,
+ # message=warning.message
+ # )
+
+ logger.info(f"已发送预警通知: {task.task_no} - {warning.message}")
+ return True
+
+ except Exception as e:
+ logger.error(f"发送预警通知失败: {str(e)}")
+ return False
+
+
+def check_delivery_warnings(db: Session) -> dict:
+ """执行配送时效预警检查(供外部调用)"""
+ task = DeliveryWarningTask(db)
+ return task.check_delivery_warnings()
+
+
+def cleanup_expired_warnings(db: Session, days: int = 7) -> int:
+ """清理过期的预警记录
+
+ Args:
+ db: 数据库会话
+ days: 保留天数,默认7天
+
+ Returns:
+ int: 删除的记录数
+ """
+ from datetime import timedelta
+
+ cutoff_date = datetime.now() - timedelta(days=days)
+
+ count = db.query(BizDeliveryWarning).filter(
+ BizDeliveryWarning.create_at < cutoff_date,
+ BizDeliveryWarning.is_resolved == 1,
+ BizDeliveryWarning.is_del == 0
+ ).update({"is_del": 1})
+
+ db.commit()
+ logger.info(f"已清理 {count} 条过期预警记录")
+ return count
diff --git a/backend/app/tasks/order_auto_confirm_receive.py b/backend/app/tasks/order_auto_confirm_receive.py
new file mode 100644
index 00000000..f796dfc2
--- /dev/null
+++ b/backend/app/tasks/order_auto_confirm_receive.py
@@ -0,0 +1,28 @@
+"""送达后超时未确认收货,系统自动确认。"""
+import logging
+from datetime import datetime, timedelta
+
+from sqlalchemy.orm import Session
+
+from app.crud.order import order_crud
+
+logger = logging.getLogger(__name__)
+
+
+def run_auto_confirm_receive_overdue(db: Session) -> int:
+ from app.api.system_config import get_auto_confirm_receive_days_for_job
+
+ days = get_auto_confirm_receive_days_for_job(db)
+ before = datetime.now() - timedelta(days=days)
+ count = order_crud.auto_confirm_receive_overdue(
+ db,
+ before_threshold=before,
+ configured_days=days,
+ )
+ return count
+
+
+def run_auto_confirm_receive_job(db: Session):
+ count = run_auto_confirm_receive_overdue(db)
+ if count:
+ logger.info(f"自动确认收货完成: {count} 条")
diff --git a/backend/app/tasks/order_payment_timeout.py b/backend/app/tasks/order_payment_timeout.py
new file mode 100644
index 00000000..152a748c
--- /dev/null
+++ b/backend/app/tasks/order_payment_timeout.py
@@ -0,0 +1,27 @@
+"""订单支付超时自动关闭任务。"""
+import logging
+from datetime import datetime, timedelta
+
+from sqlalchemy.orm import Session
+
+from app.crud.order import order_crud
+
+logger = logging.getLogger(__name__)
+
+
+def close_timeout_online_orders(db: Session) -> int:
+ """关闭超时未支付的线上订单(超时分钟数来自系统设置 auto_cancel_minutes)。"""
+ from app.api.system_config import get_auto_cancel_minutes_for_job
+
+ timeout_minutes = get_auto_cancel_minutes_for_job(db)
+ before_time = datetime.now() - timedelta(minutes=timeout_minutes)
+ timeout_note = f"超过未支付自动取消时限({timeout_minutes} 分钟)"
+ return order_crud.close_unpaid_online_orders(
+ db, before_time=before_time, timeout_note=timeout_note
+ )
+
+
+def run_close_timeout_online_orders(db: Session):
+ """定时任务包装。"""
+ count = close_timeout_online_orders(db)
+ logger.info(f"线上支付超时订单自动关闭完成: {count} 条")
diff --git a/backend/app/ws/__init__.py b/backend/app/ws/__init__.py
new file mode 100644
index 00000000..65fc5097
--- /dev/null
+++ b/backend/app/ws/__init__.py
@@ -0,0 +1,2 @@
+"""WebSocket modules."""
+
diff --git a/backend/app/ws/price_updates.py b/backend/app/ws/price_updates.py
new file mode 100644
index 00000000..85ecb911
--- /dev/null
+++ b/backend/app/ws/price_updates.py
@@ -0,0 +1,187 @@
+"""WebSocket:区域价格清单更新推送(客户端实时刷新)"""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+from dataclasses import dataclass
+from typing import Dict, Set, Optional, Any
+
+import anyio
+from fastapi import WebSocket, WebSocketDisconnect
+
+from app.core.database import SessionLocal
+from app.core.auth import get_current_user_by_token
+
+logger = logging.getLogger(__name__)
+
+@dataclass(frozen=True)
+class PriceListUpdatedEvent:
+ regionId: str
+ priceListId: str
+ priceVersion: int
+ publishedTime: Optional[str] = None
+
+ def to_message(self) -> dict:
+ return {
+ "event": "price_list_updated",
+ "data": {
+ "regionId": self.regionId,
+ "priceListId": self.priceListId,
+ "priceVersion": self.priceVersion,
+ "publishedTime": self.publishedTime,
+ },
+ }
+
+
+class RegionWSManager:
+ """按 region_id 管理 websocket 连接并广播。"""
+
+ def __init__(self) -> None:
+ self._lock = asyncio.Lock()
+ self._rooms: Dict[str, Set[WebSocket]] = {}
+
+ async def connect(self, websocket: WebSocket, *, token: str) -> dict:
+ """鉴权并接入房间。返回 current_user dict。"""
+ # 验证 token(包含 Redis 会话校验 + DB 用户校验)
+ db = SessionLocal()
+ try:
+ current_user = await get_current_user_by_token(token, db)
+ finally:
+ db.close()
+
+ region_id = (current_user.get("region_id") or "").strip()
+ if not region_id:
+ logger.warning("[ws-mini] reject: empty region_id user=%s", current_user.get("id"))
+ await websocket.close(code=1008)
+ raise RuntimeError("user has no region_id")
+
+ await websocket.accept()
+
+ async with self._lock:
+ self._rooms.setdefault(region_id, set()).add(websocket)
+ room_size = len(self._rooms.get(region_id, set()))
+ logger.warning(
+ "[ws-mini] connected user=%s role=%s region=%s room_size=%s",
+ current_user.get("id"),
+ current_user.get("role"),
+ region_id,
+ room_size,
+ )
+
+ return current_user
+
+ async def disconnect(self, websocket: WebSocket, *, region_id: str) -> None:
+ async with self._lock:
+ s = self._rooms.get(region_id)
+ if not s:
+ return
+ if websocket in s:
+ s.remove(websocket)
+ room_size = len(s)
+ if not s:
+ self._rooms.pop(region_id, None)
+ logger.warning("[ws-mini] disconnected region=%s room_size=%s", region_id, room_size if "room_size" in locals() else 0)
+
+ async def broadcast_region(self, region_id: str, message: dict) -> int:
+ """向 region 房间广播消息。返回尝试发送的连接数。"""
+ async with self._lock:
+ targets = list(self._rooms.get(region_id, set()))
+
+ if not targets:
+ logger.warning("[ws-mini] broadcast skip: no targets region=%s event=%s", region_id, message.get("event"))
+ return 0
+
+ logger.warning(
+ "[ws-mini] broadcasting region=%s event=%s target_count=%s",
+ region_id,
+ message.get("event"),
+ len(targets),
+ )
+ dead: list[WebSocket] = []
+ for ws in targets:
+ try:
+ await ws.send_json(message)
+ except Exception:
+ dead.append(ws)
+
+ if dead:
+ async with self._lock:
+ room = self._rooms.get(region_id)
+ if room:
+ for ws in dead:
+ room.discard(ws)
+ if not room:
+ self._rooms.pop(region_id, None)
+
+ return len(targets)
+
+
+manager = RegionWSManager()
+
+
+async def ws_mini_price_updates(websocket: WebSocket) -> None:
+ """小程序端 WebSocket 入口:/ws/mini"""
+ token = (websocket.query_params.get("token") or "").strip()
+ if not token:
+ # 兼容 Authorization: Bearer
+ auth = (websocket.headers.get("authorization") or "").strip()
+ if auth.lower().startswith("bearer "):
+ token = auth[7:].strip()
+
+ if not token:
+ logger.warning("[ws-mini] reject: missing token ip=%s", websocket.client.host if websocket.client else "")
+ await websocket.close(code=1008)
+ return
+
+ try:
+ current_user = await manager.connect(websocket, token=token)
+ except Exception:
+ logger.exception("[ws-mini] connect failed")
+ # connect 内部会 close;这里兜底
+ try:
+ await websocket.close(code=1008)
+ except Exception:
+ pass
+ return
+
+ region_id = (current_user.get("region_id") or "").strip()
+ try:
+ # 当前实现:仅服务器推送;客户端无需主动发消息。
+ while True:
+ await websocket.receive_text()
+ except WebSocketDisconnect:
+ logger.warning("[ws-mini] client disconnected user=%s region=%s", current_user.get("id"), region_id)
+ pass
+ except Exception:
+ logger.exception("[ws-mini] receive loop error user=%s region=%s", current_user.get("id"), region_id)
+ pass
+ finally:
+ await manager.disconnect(websocket, region_id=region_id)
+
+
+def notify_price_list_updated(event: PriceListUpdatedEvent) -> None:
+ """线程/同步上下文安全:广播价格更新事件。"""
+
+ async def _run():
+ sent = await manager.broadcast_region(event.regionId, event.to_message())
+ logger.warning(
+ "[ws-mini] notify price update region=%s price_list_id=%s version=%s sent=%s",
+ event.regionId,
+ event.priceListId,
+ event.priceVersion,
+ sent,
+ )
+
+ try:
+ loop = asyncio.get_running_loop()
+ loop.create_task(_run())
+ except RuntimeError:
+ # sync endpoint 常在 worker thread:使用 anyio 跳回事件循环
+ anyio.from_thread.run(_run)
+
+
+def register_ws_routes(app: Any) -> None:
+ """在 FastAPI app 上注册 websocket 路由。"""
+ app.websocket("/ws/mini")(ws_mini_price_updates)
+
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 00000000..e4054aa6
--- /dev/null
+++ b/backend/requirements.txt
@@ -0,0 +1,17 @@
+fastapi==0.109.0
+uvicorn[standard]==0.27.0
+sqlalchemy==2.0.25
+pymysql==1.1.0
+pydantic==2.5.3
+pydantic-settings==2.1.0
+python-jose[cryptography]==3.3.0
+cryptography>=42.0.0
+bcrypt==4.0.1
+python-multipart==0.0.6
+alembic==1.13.1
+redis==5.0.1
+httpx==0.26.0
+python-dotenv==1.0.0
+apscheduler==3.10.4
+minio==7.2.20
+Pillow>=10.0.0
diff --git a/backend/scripts/add_api_request_logs_perf_indexes.sql b/backend/scripts/add_api_request_logs_perf_indexes.sql
new file mode 100644
index 00000000..100fd102
--- /dev/null
+++ b/backend/scripts/add_api_request_logs_perf_indexes.sql
@@ -0,0 +1,17 @@
+-- /api/web/admin/api-request-logs 列表性能索引补强
+-- 典型查询:
+-- WHERE is_del=0 [AND http_method=?] [AND status_code=?]
+-- [AND create_at between ? and ?]
+-- ORDER BY create_at DESC
+
+-- 默认列表 + 时间倒序分页
+ALTER TABLE sys_api_request_log
+ ADD INDEX idx_arl_isdel_createat (is_del, create_at);
+
+-- 按方法过滤
+ALTER TABLE sys_api_request_log
+ ADD INDEX idx_arl_isdel_method_createat (is_del, http_method, create_at);
+
+-- 按状态码过滤
+ALTER TABLE sys_api_request_log
+ ADD INDEX idx_arl_isdel_status_createat (is_del, status_code, create_at);
diff --git a/backend/scripts/add_bas_product_list_indexes.sql b/backend/scripts/add_bas_product_list_indexes.sql
new file mode 100644
index 00000000..cf973636
--- /dev/null
+++ b/backend/scripts/add_bas_product_list_indexes.sql
@@ -0,0 +1,12 @@
+-- 给 /api/web/admin/products 列表(GET,前端加密传输时为 POST + X-HTTP-Method-Override: GET)做索引补强
+-- 典型 SQL:WHERE is_del = 0 [AND category_id = ?] [AND (name LIKE ... OR description LIKE ...)] ORDER BY create_at DESC
+-- LIKE '%keyword%' 无法有效走 name/description 索引;无关键词时下面索引可帮助 COUNT + 分页排序。
+-- 执行前请先 SHOW INDEX FROM bas_product; 若索引已存在可跳过对应语句。
+
+-- 1) 全量列表 / 仅启用筛选:is_del + 排序字段
+ALTER TABLE bas_product
+ ADD INDEX idx_product_isdel_createat (is_del, create_at);
+
+-- 2) 按分类筛选 + 排序(与 1 并存;分类列表走此索引更省扫描)
+ALTER TABLE bas_product
+ ADD INDEX idx_product_isdel_category_createat (is_del, category_id, create_at);
diff --git a/backend/scripts/add_delivery_tasks_perf_indexes.sql b/backend/scripts/add_delivery_tasks_perf_indexes.sql
new file mode 100644
index 00000000..81cf466d
--- /dev/null
+++ b/backend/scripts/add_delivery_tasks_perf_indexes.sql
@@ -0,0 +1,26 @@
+-- /api/web/delivery-tasks 列表性能索引补强
+-- 目标查询特征:
+-- 1) WHERE is_del=0 ORDER BY create_at DESC(默认列表)
+-- 2) WHERE is_del=0 AND region_id=? AND status=? ORDER BY create_at DESC
+-- 3) WHERE is_del=0 AND deliver_id=? ORDER BY create_at DESC
+-- 4) 列表中按 task_id 聚合明细数量(biz_delivery_task_item)
+
+-- 1) 默认列表与时间倒序分页
+ALTER TABLE biz_delivery_task
+ ADD INDEX idx_dt_isdel_createat (is_del, create_at);
+
+-- 2) 区域 + 状态过滤场景
+ALTER TABLE biz_delivery_task
+ ADD INDEX idx_dt_isdel_region_status_createat (is_del, region_id, status, create_at);
+
+-- 3) 配送员维度过滤场景
+ALTER TABLE biz_delivery_task
+ ADD INDEX idx_dt_isdel_deliver_createat (is_del, deliver_id, create_at);
+
+-- 4) 供应商维度过滤场景(供应商端任务列表)
+ALTER TABLE biz_delivery_task
+ ADD INDEX idx_dt_isdel_supplier_createat (is_del, supplier_id, create_at);
+
+-- 5) 列表 itemCount 聚合:WHERE task_id IN (...) AND is_del=0 GROUP BY task_id
+ALTER TABLE biz_delivery_task_item
+ ADD INDEX idx_dti_task_isdel (task_id, is_del);
diff --git a/backend/scripts/add_orders_perf_indexes.sql b/backend/scripts/add_orders_perf_indexes.sql
new file mode 100644
index 00000000..42a9af71
--- /dev/null
+++ b/backend/scripts/add_orders_perf_indexes.sql
@@ -0,0 +1,21 @@
+-- 给 /api/web/admin/orders 列表接口做性能索引补强(幂等尽量实现)
+-- 注意:不同 MySQL 版本对 IF NOT EXISTS 支持不同;若执行失败,请先 SHOW INDEX 再手动判断是否已存在。
+
+-- 1) 覆盖最常见列表:WHERE is_del=0 ORDER BY create_at DESC
+-- 目标:减少 Using filesort / 全表扫描风险
+ALTER TABLE biz_order
+ ADD INDEX idx_order_isdel_createat (is_del, create_at);
+
+-- 2) 常用筛选:区域 + 订单主状态 + 时间排序
+ALTER TABLE biz_order
+ ADD INDEX idx_order_isdel_region_orderstatus_createat (is_del, region_id, order_status, create_at);
+
+-- 3) 常用筛选:区域 + 创建时间范围(startDate/endDate)
+ALTER TABLE biz_order
+ ADD INDEX idx_order_isdel_region_createat (is_del, region_id, create_at);
+
+-- 4) 单号查询建议:
+-- 当前实现是 order_no LIKE '%xxx%',该写法通常无法走索引。
+-- 若业务可改为前缀匹配(LIKE 'xxx%')或精确匹配(=),将显著提升性能。
+-- 订单号已有唯一索引 uk_order_no(可用于 '=' 或前缀匹配)。
+
diff --git a/backend/scripts/add_price_versions_perf_indexes.sql b/backend/scripts/add_price_versions_perf_indexes.sql
new file mode 100644
index 00000000..9029bccd
--- /dev/null
+++ b/backend/scripts/add_price_versions_perf_indexes.sql
@@ -0,0 +1,20 @@
+-- /api/web/admin/price-versions 列表性能索引补强
+-- 典型查询:
+-- WHERE is_del=0 [AND region_id=?] [AND supplier_id=?] [AND status=?]
+-- ORDER BY create_at DESC
+
+-- 默认列表 + 时间倒序分页
+ALTER TABLE biz_price_list
+ ADD INDEX idx_pl_isdel_createat (is_del, create_at);
+
+-- 区域维度过滤
+ALTER TABLE biz_price_list
+ ADD INDEX idx_pl_isdel_region_createat (is_del, region_id, create_at);
+
+-- 供应商维度过滤
+ALTER TABLE biz_price_list
+ ADD INDEX idx_pl_isdel_supplier_createat (is_del, supplier_id, create_at);
+
+-- 状态维度过滤
+ALTER TABLE biz_price_list
+ ADD INDEX idx_pl_isdel_status_createat (is_del, status, create_at);
diff --git a/backend/scripts/add_sys_region_active_index.sql b/backend/scripts/add_sys_region_active_index.sql
new file mode 100644
index 00000000..3ee11e13
--- /dev/null
+++ b/backend/scripts/add_sys_region_active_index.sql
@@ -0,0 +1,7 @@
+-- 询价报价界面“区域”下拉/Tab:启用区域列表索引补强
+-- 对应接口:GET /api/web/admin/regions/active
+-- 查询:WHERE is_del=0 AND is_active=1 ORDER BY sort_order
+
+ALTER TABLE sys_region
+ ADD INDEX idx_region_isdel_active_sort (is_del, is_active, sort_order);
+
diff --git a/backend/scripts/clear_biz_orders.py b/backend/scripts/clear_biz_orders.py
new file mode 100644
index 00000000..25a7f485
--- /dev/null
+++ b/backend/scripts/clear_biz_orders.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+"""清空订单及相关表(开发调试用)。用法:在 backend 目录执行 python scripts/clear_biz_orders.py"""
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+from sqlalchemy import text
+
+from app.core.database import engine
+
+TABLES = [
+ "biz_order_status_log_his",
+ "biz_order_status_log",
+ "biz_order_item",
+ "biz_order_his",
+ "biz_order_adjust_his",
+ "biz_order_adjust",
+ "biz_wechat_pay",
+ "biz_refund_his",
+ "biz_refund",
+ "biz_settlement_his",
+ "biz_settlement",
+ "biz_invoice",
+ "biz_deliverer_review_his",
+ "biz_deliverer_review",
+ "biz_delivery_warning_his",
+ "biz_delivery_warning",
+ "biz_delivery_task_item",
+ "biz_delivery_task_his",
+ "biz_delivery_task",
+ "biz_order",
+]
+
+
+def main() -> None:
+ with engine.begin() as conn:
+ conn.execute(text("SET FOREIGN_KEY_CHECKS=0"))
+ for t in TABLES:
+ conn.execute(text(f"TRUNCATE TABLE `{t}`"))
+ conn.execute(text("SET FOREIGN_KEY_CHECKS=1"))
+ print("已清空订单及相关表:", ", ".join(TABLES))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/backend/scripts/migrate_minio_urls.py b/backend/scripts/migrate_minio_urls.py
new file mode 100644
index 00000000..ac6067aa
--- /dev/null
+++ b/backend/scripts/migrate_minio_urls.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+"""批量替换数据库历史 URL 前缀(MinIO 域名迁移脚本)。
+
+用法(在 backend 目录执行):
+1) 先预览:
+ python scripts/migrate_minio_urls.py --dry-run
+
+2) 正式执行:
+ python scripts/migrate_minio_urls.py
+
+3) 自定义前缀:
+ python scripts/migrate_minio_urls.py \
+ --old-prefix "https://www.chengyantek.cn:20004" \
+ --new-prefix "https://www.chengyantek.cn"
+"""
+from __future__ import annotations
+
+import argparse
+import sys
+from datetime import datetime
+from pathlib import Path
+
+from sqlalchemy import text
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+from app.core.database import engine # noqa: E402
+
+
+DEFAULT_OLD_PREFIX = "https://www.chengyantek.cn:20004"
+DEFAULT_NEW_PREFIX = "https://www.chengyantek.cn"
+
+TEXT_TYPES = (
+ "char",
+ "varchar",
+ "tinytext",
+ "text",
+ "mediumtext",
+ "longtext",
+)
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description="批量替换数据库中的 URL 前缀")
+ parser.add_argument("--old-prefix", default=DEFAULT_OLD_PREFIX, help="旧 URL 前缀")
+ parser.add_argument("--new-prefix", default=DEFAULT_NEW_PREFIX, help="新 URL 前缀")
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="仅预览受影响记录数,不执行更新",
+ )
+ return parser.parse_args()
+
+
+def quote_ident(name: str) -> str:
+ """安全引用 MySQL 标识符。"""
+ return f"`{name.replace('`', '``')}`"
+
+
+def get_text_columns(conn) -> list[tuple[str, str]]:
+ sql = text(
+ """
+ SELECT table_name, column_name
+ FROM information_schema.columns
+ WHERE table_schema = DATABASE()
+ AND data_type IN :types
+ ORDER BY table_name, ordinal_position
+ """
+ )
+ rows = conn.execute(sql, {"types": TEXT_TYPES}).fetchall()
+ return [(r[0], r[1]) for r in rows]
+
+
+def count_affected(conn, table_name: str, column_name: str, old_prefix: str) -> int:
+ sql = text(
+ f"""
+ SELECT COUNT(*) AS cnt
+ FROM {quote_ident(table_name)}
+ WHERE {quote_ident(column_name)} LIKE :pattern
+ """
+ )
+ return int(conn.execute(sql, {"pattern": f"%{old_prefix}%"}).scalar() or 0)
+
+
+def update_column(
+ conn,
+ table_name: str,
+ column_name: str,
+ old_prefix: str,
+ new_prefix: str,
+) -> int:
+ sql = text(
+ f"""
+ UPDATE {quote_ident(table_name)}
+ SET {quote_ident(column_name)} = REPLACE({quote_ident(column_name)}, :old_prefix, :new_prefix)
+ WHERE {quote_ident(column_name)} LIKE :pattern
+ """
+ )
+ result = conn.execute(
+ sql,
+ {
+ "old_prefix": old_prefix,
+ "new_prefix": new_prefix,
+ "pattern": f"%{old_prefix}%",
+ },
+ )
+ return int(result.rowcount or 0)
+
+
+def make_backup_table_name(table_name: str, suffix: str) -> str:
+ """生成备份表名,控制在 MySQL 64 字符限制内。"""
+ max_table_len = 64
+ reserve = len("_bak_") + len(suffix)
+ max_base_len = max_table_len - reserve
+ base = table_name[:max_base_len]
+ return f"{base}_bak_{suffix}"
+
+
+def backup_table(conn, table_name: str, backup_table_name: str) -> None:
+ """备份单张表(结构+数据)。"""
+ conn.execute(
+ text(
+ f"CREATE TABLE {quote_ident(backup_table_name)} LIKE {quote_ident(table_name)}"
+ )
+ )
+ conn.execute(
+ text(
+ f"INSERT INTO {quote_ident(backup_table_name)} SELECT * FROM {quote_ident(table_name)}"
+ )
+ )
+
+
+def main() -> None:
+ args = parse_args()
+ old_prefix = args.old_prefix.strip().rstrip("/")
+ new_prefix = args.new_prefix.strip().rstrip("/")
+ dry_run = args.dry_run
+
+ if not old_prefix or not new_prefix:
+ raise ValueError("old-prefix/new-prefix 不能为空")
+ if old_prefix == new_prefix:
+ raise ValueError("old-prefix 与 new-prefix 不能相同")
+
+ print(f"数据库 URL 迁移开始,模式:{'预览(dry-run)' if dry_run else '正式执行'}")
+ print(f"旧前缀: {old_prefix}")
+ print(f"新前缀: {new_prefix}")
+
+ total_rows = 0
+ hit_columns = 0
+ affected_items: list[tuple[str, str, int]] = []
+
+ with engine.begin() as conn:
+ columns = get_text_columns(conn)
+ for table_name, column_name in columns:
+ affected = count_affected(conn, table_name, column_name, old_prefix)
+ if affected <= 0:
+ continue
+ affected_items.append((table_name, column_name, affected))
+
+ if not affected_items:
+ print("未发现需要替换的数据,已结束。")
+ return
+
+ if not dry_run:
+ backup_suffix = datetime.now().strftime("%Y%m%d%H%M%S")
+ backup_tables: dict[str, str] = {}
+ for table_name, _, _ in affected_items:
+ if table_name in backup_tables:
+ continue
+ backup_name = make_backup_table_name(table_name, backup_suffix)
+ backup_table(conn, table_name, backup_name)
+ backup_tables[table_name] = backup_name
+ print(f"[BACKUP] {table_name} -> {backup_name}")
+
+ print(f"备份完成:共备份 {len(backup_tables)} 张表。")
+
+ for table_name, column_name, affected in affected_items:
+ hit_columns += 1
+ if dry_run:
+ print(f"[DRY-RUN] {table_name}.{column_name} -> {affected} 行")
+ total_rows += affected
+ continue
+
+ updated = update_column(conn, table_name, column_name, old_prefix, new_prefix)
+ print(f"[UPDATED] {table_name}.{column_name} -> {updated} 行")
+ total_rows += updated
+
+ if dry_run:
+ print(f"预览完成:共 {hit_columns} 个字段命中,预计影响 {total_rows} 行。")
+ else:
+ print(f"执行完成:共更新 {hit_columns} 个字段,影响 {total_rows} 行。")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/backend/scripts/migration_20260429_user_scope_menu_code.sql b/backend/scripts/migration_20260429_user_scope_menu_code.sql
new file mode 100644
index 00000000..41a3bce0
--- /dev/null
+++ b/backend/scripts/migration_20260429_user_scope_menu_code.sql
@@ -0,0 +1,68 @@
+-- 运营端权限改造:super_admin + 多区域 + 菜单 code 授权
+
+-- 1) 角色迁移:admin -> super_admin
+UPDATE sys_user
+SET role = 'super_admin',
+ update_at = NOW()
+WHERE is_del = 0
+ AND role = 'admin';
+
+-- 2) 用户-区域权限表
+CREATE TABLE IF NOT EXISTS sys_user_region_scope (
+ id VARCHAR(32) NOT NULL COMMENT '主键ID',
+ user_id VARCHAR(32) NOT NULL COMMENT '用户ID',
+ region_id VARCHAR(32) NOT NULL COMMENT '区域ID',
+ is_del TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除',
+ create_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ create_by VARCHAR(32) DEFAULT NULL COMMENT '创建人ID',
+ create_name VARCHAR(100) DEFAULT NULL COMMENT '创建人姓名',
+ update_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+ update_by VARCHAR(32) DEFAULT NULL COMMENT '修改人ID',
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_user_region_scope (user_id, region_id, is_del),
+ KEY idx_user_region_scope_user (user_id, is_del),
+ KEY idx_user_region_scope_region (region_id, is_del)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户区域权限';
+
+-- 3) 用户-区域权限历史表(无索引、无 is_del)
+CREATE TABLE IF NOT EXISTS sys_user_region_scope_his (
+ id VARCHAR(32) NOT NULL COMMENT '主键ID',
+ user_id VARCHAR(32) NOT NULL COMMENT '用户ID',
+ region_id VARCHAR(32) NOT NULL COMMENT '区域ID',
+ create_at DATETIME DEFAULT NULL COMMENT '创建时间',
+ create_by VARCHAR(32) DEFAULT NULL COMMENT '创建人ID',
+ create_name VARCHAR(100) DEFAULT NULL COMMENT '创建人姓名',
+ update_at DATETIME DEFAULT NULL COMMENT '修改时间',
+ update_by VARCHAR(32) DEFAULT NULL COMMENT '修改人ID',
+ his_create_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '历史记录创建时间'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户区域权限历史';
+
+-- 4) 用户-菜单 code 权限表
+CREATE TABLE IF NOT EXISTS sys_user_menu_scope (
+ id VARCHAR(32) NOT NULL COMMENT '主键ID',
+ user_id VARCHAR(32) NOT NULL COMMENT '用户ID',
+ menu_code VARCHAR(100) NOT NULL COMMENT '菜单编码',
+ is_del TINYINT NOT NULL DEFAULT 0 COMMENT '是否删除',
+ create_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ create_by VARCHAR(32) DEFAULT NULL COMMENT '创建人ID',
+ create_name VARCHAR(100) DEFAULT NULL COMMENT '创建人姓名',
+ update_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+ update_by VARCHAR(32) DEFAULT NULL COMMENT '修改人ID',
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_user_menu_scope (user_id, menu_code, is_del),
+ KEY idx_user_menu_scope_user (user_id, is_del),
+ KEY idx_user_menu_scope_code (menu_code, is_del)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户菜单权限';
+
+-- 5) 用户-菜单 code 权限历史表(无索引、无 is_del)
+CREATE TABLE IF NOT EXISTS sys_user_menu_scope_his (
+ id VARCHAR(32) NOT NULL COMMENT '主键ID',
+ user_id VARCHAR(32) NOT NULL COMMENT '用户ID',
+ menu_code VARCHAR(100) NOT NULL COMMENT '菜单编码',
+ create_at DATETIME DEFAULT NULL COMMENT '创建时间',
+ create_by VARCHAR(32) DEFAULT NULL COMMENT '创建人ID',
+ create_name VARCHAR(100) DEFAULT NULL COMMENT '创建人姓名',
+ update_at DATETIME DEFAULT NULL COMMENT '修改时间',
+ update_by VARCHAR(32) DEFAULT NULL COMMENT '修改人ID',
+ his_create_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '历史记录创建时间'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户菜单权限历史';
diff --git a/backend/start-local.sh b/backend/start-local.sh
new file mode 100755
index 00000000..6d0f13b8
--- /dev/null
+++ b/backend/start-local.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+set -euo pipefail
+
+BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$BASE_DIR"
+
+ENV_FILE="${ENV_FILE:-$BASE_DIR/.env.local}"
+HOST="${HOST:-127.0.0.1}"
+PORT="${PORT:-8000}"
+APP_MODULE="${APP_MODULE:-app.main:app}"
+VENV_DIR="${VENV_DIR:-$BASE_DIR/venv}"
+UVICORN_BIN="${UVICORN_BIN:-$VENV_DIR/bin/uvicorn}"
+
+if [ ! -f "$ENV_FILE" ]; then
+ echo "[ERROR] 本地环境文件不存在: $ENV_FILE"
+ echo "请先创建并填写 backend/.env.local"
+ exit 1
+fi
+
+if [ ! -x "$UVICORN_BIN" ]; then
+ echo "[ERROR] 未找到 uvicorn: $UVICORN_BIN"
+ echo "请先创建虚拟环境并安装依赖:"
+ echo " python3 -m venv venv"
+ echo " source venv/bin/activate"
+ echo " pip install -r requirements.txt"
+ exit 1
+fi
+
+echo "[INFO] 启动本地开发服务"
+echo "[INFO] ENV_FILE=$ENV_FILE"
+echo "[INFO] URL=http://$HOST:$PORT"
+
+ENV_FILE="$ENV_FILE" "$UVICORN_BIN" "$APP_MODULE" --host "$HOST" --port "$PORT" --reload --log-level debug
diff --git a/backend/start-prod.sh b/backend/start-prod.sh
new file mode 100755
index 00000000..0b337f76
--- /dev/null
+++ b/backend/start-prod.sh
@@ -0,0 +1,254 @@
+#!/bin/bash
+
+# ============================================
+# 陇耘鲜达 Backend 生产环境启动脚本
+# 适配: python3.6+ (显式不使用 python2)
+# ============================================
+
+set -euo pipefail
+
+BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$BASE_DIR"
+
+APP_MODULE="${APP_MODULE:-app.main:app}"
+HOST="${HOST:-0.0.0.0}"
+PORT="${PORT:-8000}"
+WORKERS="${WORKERS:-2}"
+LOG_LEVEL="${LOG_LEVEL:-info}"
+ENV_FILE="${ENV_FILE:-$BASE_DIR/.env.production}"
+
+VENV_DIR="${VENV_DIR:-$BASE_DIR/venv}"
+PYTHON_BIN="${PYTHON_BIN:-$VENV_DIR/bin/python}"
+UVICORN_BIN="${UVICORN_BIN:-$VENV_DIR/bin/uvicorn}"
+
+LOG_DIR="$BASE_DIR/logs"
+LOG_FILE="$LOG_DIR/backend-prod.log"
+PID_FILE="$BASE_DIR/backend-prod.pid"
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+log_info() {
+ echo -e "${GREEN}[INFO]${NC} $1"
+}
+
+log_warn() {
+ echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+show_help() {
+ cat << 'EOF'
+用法:
+ ./start-prod.sh [start|stop|restart|status|logs]
+
+命令:
+ start 启动服务(默认)
+ stop 停止服务
+ restart 重启服务
+ status 查看状态
+ logs 查看日志(最近100行)
+
+可选环境变量:
+ HOST=0.0.0.0
+ PORT=8000
+ WORKERS=2
+ LOG_LEVEL=info
+ APP_MODULE=app.main:app
+ VENV_DIR=./venv
+ ENV_FILE=./.env.production
+EOF
+}
+
+ensure_dirs() {
+ mkdir -p "$LOG_DIR"
+}
+
+check_python() {
+ if ! command -v python3 >/dev/null 2>&1; then
+ log_error "未找到 python3,请安装 python3.6+"
+ exit 1
+ fi
+
+ local py_ver
+ py_ver="$(python3 -c 'import sys; print("{}.{}".format(sys.version_info[0], sys.version_info[1]))')"
+ log_info "系统 python3 版本: $py_ver"
+}
+
+ensure_venv_and_runtime() {
+ if [ ! -d "$VENV_DIR" ]; then
+ log_warn "未找到虚拟环境,开始自动创建: $VENV_DIR"
+ if ! python3 -m venv "$VENV_DIR" >/dev/null 2>&1; then
+ log_error "自动创建 venv 失败,请先安装 python3-venv 或手动创建虚拟环境"
+ log_warn "可尝试: yum install -y python3-venv 或 python3 -m ensurepip --upgrade"
+ exit 1
+ fi
+ fi
+
+ if [ ! -x "$PYTHON_BIN" ]; then
+ log_error "虚拟环境 python 不存在: $PYTHON_BIN"
+ exit 1
+ fi
+
+ if [ ! -x "$VENV_DIR/bin/pip" ]; then
+ log_warn "虚拟环境 pip 不存在,尝试引导安装"
+ "$PYTHON_BIN" -m ensurepip --upgrade >/dev/null 2>&1 || true
+ fi
+
+ if [ ! -x "$UVICORN_BIN" ]; then
+ log_warn "未检测到 uvicorn,开始安装依赖 requirements.txt"
+ if [ ! -x "$VENV_DIR/bin/pip" ]; then
+ log_error "pip 仍不可用,请手动执行依赖初始化"
+ exit 1
+ fi
+ "$VENV_DIR/bin/pip" install -r "$BASE_DIR/requirements.txt"
+ if [ ! -x "$UVICORN_BIN" ]; then
+ log_error "依赖安装后仍未找到 uvicorn: $UVICORN_BIN"
+ exit 1
+ fi
+ fi
+}
+
+is_running() {
+ if [ ! -f "$PID_FILE" ]; then
+ return 1
+ fi
+
+ local pid
+ pid="$(cat "$PID_FILE")"
+ if [ -z "$pid" ]; then
+ return 1
+ fi
+
+ if ps -p "$pid" >/dev/null 2>&1; then
+ return 0
+ fi
+
+ return 1
+}
+
+start_server() {
+ check_python
+ ensure_venv_and_runtime
+ ensure_dirs
+
+ if is_running; then
+ local running_pid
+ running_pid="$(cat "$PID_FILE")"
+ log_warn "服务已在运行 (PID: $running_pid)"
+ exit 0
+ fi
+
+ if [ -f "$PID_FILE" ]; then
+ rm -f "$PID_FILE"
+ fi
+
+ log_info "启动生产服务..."
+ log_info "监听地址: $HOST:$PORT"
+ log_info "工作进程: $WORKERS"
+ log_info "日志文件: $LOG_FILE"
+ log_info "环境文件: $ENV_FILE"
+
+ if [ ! -f "$ENV_FILE" ]; then
+ log_error "环境文件不存在: $ENV_FILE"
+ exit 1
+ fi
+
+ ENV_FILE="$ENV_FILE" nohup "$UVICORN_BIN" "$APP_MODULE" \
+ --host "$HOST" \
+ --port "$PORT" \
+ --workers "$WORKERS" \
+ --log-level "$LOG_LEVEL" \
+ >> "$LOG_FILE" 2>&1 &
+
+ echo $! > "$PID_FILE"
+ sleep 2
+
+ if is_running; then
+ log_info "服务启动成功 (PID: $(cat "$PID_FILE"))"
+ log_info "健康检查: http://127.0.0.1:$PORT/health"
+ else
+ log_error "服务启动失败,请检查日志: $LOG_FILE"
+ rm -f "$PID_FILE"
+ exit 1
+ fi
+}
+
+stop_server() {
+ if ! is_running; then
+ log_warn "服务未运行"
+ [ -f "$PID_FILE" ] && rm -f "$PID_FILE"
+ exit 0
+ fi
+
+ local pid
+ pid="$(cat "$PID_FILE")"
+ log_info "正在停止服务 (PID: $pid)"
+ kill -TERM "$pid" >/dev/null 2>&1 || true
+
+ for _ in 1 2 3 4 5 6 7 8 9 10; do
+ if ! ps -p "$pid" >/dev/null 2>&1; then
+ break
+ fi
+ sleep 1
+ done
+
+ if ps -p "$pid" >/dev/null 2>&1; then
+ log_warn "优雅停止超时,执行强制停止"
+ kill -9 "$pid" >/dev/null 2>&1 || true
+ fi
+
+ rm -f "$PID_FILE"
+ log_info "服务已停止"
+}
+
+status_server() {
+ if is_running; then
+ echo -e "${GREEN}RUNNING${NC} PID=$(cat "$PID_FILE") HOST=$HOST PORT=$PORT"
+ else
+ echo -e "${RED}STOPPED${NC}"
+ fi
+}
+
+show_logs() {
+ ensure_dirs
+ if [ ! -f "$LOG_FILE" ]; then
+ log_warn "暂无日志文件: $LOG_FILE"
+ exit 0
+ fi
+ # 生产排障时常用最近日志
+ tail -n 100 "$LOG_FILE"
+}
+
+ACTION="${1:-start}"
+case "$ACTION" in
+ start)
+ start_server
+ ;;
+ stop)
+ stop_server
+ ;;
+ restart)
+ stop_server || true
+ start_server
+ ;;
+ status)
+ status_server
+ ;;
+ logs)
+ show_logs
+ ;;
+ -h|--help|help)
+ show_help
+ ;;
+ *)
+ log_error "未知命令: $ACTION"
+ show_help
+ exit 1
+ ;;
+esac
diff --git a/backend/start.sh b/backend/start.sh
new file mode 100755
index 00000000..de0b81bb
--- /dev/null
+++ b/backend/start.sh
@@ -0,0 +1,393 @@
+#!/bin/bash
+
+# ============================================
+# 陇耘鲜达 Backend 启动脚本
+# ============================================
+
+set -e
+
+# 颜色定义
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# 默认配置
+HOST="0.0.0.0"
+PORT=8000
+WORKERS=1
+RELOAD=false
+LOG_LEVEL="info"
+
+# 脚本目录
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+BACKEND_DIR="$SCRIPT_DIR"
+
+# 虚拟环境目录
+VENV_DIR="$BACKEND_DIR/venv"
+PIP="$VENV_DIR/bin/pip"
+PYTHON="$VENV_DIR/bin/python"
+UVICORN="$VENV_DIR/bin/uvicorn"
+
+# 日志文件
+LOG_FILE="$BACKEND_DIR/logs/app.log"
+PID_FILE="$BACKEND_DIR/app.pid"
+
+# ============================================
+# 函数定义
+# ============================================
+
+log_info() {
+ echo -e "${GREEN}[INFO]${NC} $1"
+}
+
+log_warn() {
+ echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+# 显示帮助信息
+show_help() {
+ cat << EOF
+陇耘鲜达 Backend 启动脚本
+
+用法: $0 [选项]
+
+选项:
+ -h, --host 监听地址 (默认: 0.0.0.0)
+ -p, --port 监听端口 (默认: 8000)
+ -w, --workers 工作进程数 (默认: 1)
+ -r, --reload 开发模式, 自动重载
+ -l, --log-level 日志级别: debug/info/warning/error (默认: info)
+ --install 安装依赖
+ --init 初始化环境
+ --stop 停止服务
+ --restart 重启服务
+ --status 查看服务状态
+ --logs 查看日志
+ --help 显示帮助信息
+
+示例:
+ $0 --port 8080 --reload # 开发模式, 端口 8080
+ $0 --install # 安装依赖
+ $0 --stop # 停止服务
+ $0 --logs # 查看日志
+EOF
+}
+
+# 检查 Python 是否安装
+check_python() {
+ if ! command -v python3 &> /dev/null; then
+ log_error "Python3 未安装, 请先安装 Python 3.10+"
+ exit 1
+ fi
+ PYTHON_VERSION=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))')
+ log_info "Python 版本: $PYTHON_VERSION"
+}
+
+# 创建虚拟环境
+create_venv() {
+ log_info "创建虚拟环境..."
+ python3 -m venv "$VENV_DIR"
+ log_success "虚拟环境创建完成: $VENV_DIR"
+}
+
+# 安装依赖
+install_dependencies() {
+ log_info "安装依赖..."
+
+ # 检查虚拟环境
+ if [ ! -d "$VENV_DIR" ]; then
+ create_venv
+ fi
+
+ # 激活虚拟环境
+ source "$VENV_DIR/bin/activate"
+
+ # 升级 pip
+ pip install --upgrade pip
+
+ # 安装依赖
+ pip install -r "$BACKEND_DIR/requirements.txt"
+
+ log_success "依赖安装完成"
+}
+
+# 检查环境
+check_env() {
+ # 检查虚拟环境
+ if [ ! -d "$VENV_DIR" ]; then
+ log_warn "虚拟环境不存在, 正在创建..."
+ create_venv
+ install_dependencies
+ fi
+
+ # 检查依赖
+ if ! "$PYTHON" -c "import fastapi" &> /dev/null; then
+ log_warn "依赖未安装, 正在安装..."
+ install_dependencies
+ fi
+
+ # 检查 .env 文件
+ if [ ! -f "$BACKEND_DIR/.env" ]; then
+ if [ -f "$BACKEND_DIR/.env.example" ]; then
+ log_warn ".env 文件不存在, 复制 .env.example 作为模板"
+ cp "$BACKEND_DIR/.env.example" "$BACKEND_DIR/.env"
+ log_warn "请编辑 $BACKEND_DIR/.env 配置文件"
+ else
+ log_error ".env 文件不存在且无 .env.example 模板"
+ fi
+ fi
+}
+
+# 创建日志目录
+setup_logs() {
+ if [ ! -d "$BACKEND_DIR/logs" ]; then
+ mkdir -p "$BACKEND_DIR/logs"
+ log_info "创建日志目录: $BACKEND_DIR/logs"
+ fi
+}
+
+# 启动服务
+start_server() {
+ # 检查服务是否已运行
+ if [ -f "$PID_FILE" ]; then
+ PID=$(cat "$PID_FILE")
+ if ps -p "$PID" &> /dev/null; then
+ log_error "服务已在运行 (PID: $PID), 请先停止后再启动"
+ exit 1
+ else
+ log_warn "发现旧的 PID 文件, 已清理"
+ rm -f "$PID_FILE"
+ fi
+ fi
+
+ # 激活虚拟环境
+ source "$VENV_DIR/bin/activate"
+
+ # 构建 uvicorn 命令
+ UVICORN_CMD="uvicorn app.main:app"
+ UVICORN_CMD="$UVICORN_CMD --host $HOST"
+ UVICORN_CMD="$UVICORN_CMD --port $PORT"
+ UVICORN_CMD="$UVICORN_CMD --log-level $LOG_LEVEL"
+
+ if [ "$RELOAD" = true ]; then
+ UVICORN_CMD="$UVICORN_CMD --reload"
+ log_info "启动模式: 开发模式 (自动重载)"
+ else
+ UVICORN_CMD="$UVICORN_CMD --workers $WORKERS"
+ log_info "启动模式: 生产模式 (Workers: $WORKERS)"
+ fi
+
+ # 创建日志目录
+ setup_logs
+
+ # 切换到后端目录
+ cd "$BACKEND_DIR"
+
+ # 记录启动信息
+ echo "========================================" >> "$LOG_FILE"
+ echo "启动时间: $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"
+ echo "启动参数: $UVICORN_CMD" >> "$LOG_FILE"
+ echo "========================================" >> "$LOG_FILE"
+
+ # 启动服务
+ log_info "=========================================="
+ log_info " 陇耘鲜达 Backend 启动中..."
+ log_info "=========================================="
+ log_info " 监听地址: $HOST:$PORT"
+ log_info " 日志级别: $LOG_LEVEL"
+ log_info " 日志文件: $LOG_FILE"
+ log_info " 文档地址: http://$HOST:$PORT/docs"
+ log_info "=========================================="
+
+ # 后台运行并保存 PID
+ nohup $UVICORN_CMD >> "$LOG_FILE" 2>&1 &
+ echo $! > "$PID_FILE"
+
+ # 等待服务启动
+ sleep 2
+
+ # 检查服务是否启动成功
+ if ps -p $(cat "$PID_FILE") &> /dev/null; then
+ log_success "服务启动成功 (PID: $(cat "$PID_FILE"))"
+ log_info "访问地址: http://localhost:$PORT"
+ log_info "API 文档: http://localhost:$PORT/docs"
+ else
+ log_error "服务启动失败, 请查看日志: $LOG_FILE"
+ rm -f "$PID_FILE"
+ exit 1
+ fi
+}
+
+# 停止服务
+stop_server() {
+ if [ ! -f "$PID_FILE" ]; then
+ log_error "PID 文件不存在, 服务可能未运行"
+ exit 1
+ fi
+
+ PID=$(cat "$PID_FILE")
+
+ if ! ps -p "$PID" &> /dev/null; then
+ log_warn "服务未在运行 (PID: $PID)"
+ rm -f "$PID_FILE"
+ exit 0
+ fi
+
+ log_info "正在停止服务 (PID: $PID)..."
+
+ # 尝试优雅关闭
+ kill -TERM "$PID" 2>/dev/null
+
+ # 等待进程退出
+ for i in {1..10}; do
+ if ! ps -p "$PID" &> /dev/null; then
+ break
+ fi
+ sleep 1
+ done
+
+ # 强制关闭
+ if ps -p "$PID" &> /dev/null; then
+ log_warn "优雅关闭超时, 强制关闭..."
+ kill -9 "$PID" 2>/dev/null
+ fi
+
+ rm -f "$PID_FILE"
+ log_success "服务已停止"
+}
+
+# 查看状态
+show_status() {
+ if [ ! -f "$PID_FILE" ]; then
+ echo -e "${YELLOW}[WARN]${NC} 服务未运行 (无 PID 文件)"
+ exit 0
+ fi
+
+ PID=$(cat "$PID_FILE")
+
+ if ps -p "$PID" &> /dev/null; then
+ echo -e "${GREEN}[RUNNING]${NC} 服务运行中 (PID: $PID)"
+
+ # 获取端口信息
+ if command -v lsof &> /dev/null; then
+ PORT_INFO=$(lsof -i :$PORT 2>/dev/null | grep LISTEN | head -1)
+ if [ -n "$PORT_INFO" ]; then
+ echo ""
+ echo "端口信息:"
+ lsof -i :$PORT 2>/dev/null | grep LISTEN | head -5
+ fi
+ fi
+ else
+ echo -e "${RED}[STOPPED]${NC} 服务已停止"
+ rm -f "$PID_FILE"
+ fi
+}
+
+# 查看日志
+show_logs() {
+ if [ ! -f "$LOG_FILE" ]; then
+ log_error "日志文件不存在: $LOG_FILE"
+ exit 1
+ fi
+
+ if [ "$1" = "-f" ] || [ "$1" = "--follow" ]; then
+ log_info "实时查看日志 (Ctrl+C 退出)..."
+ tail -f "$LOG_FILE"
+ else
+ echo -e "${BLUE}=== 最近 100 行日志 ===${NC}"
+ tail -100 "$LOG_FILE"
+ fi
+}
+
+# 重启服务
+restart_server() {
+ log_info "重启服务..."
+ stop_server
+ sleep 2
+ start_server
+}
+
+# 初始化环境
+init_env() {
+ check_python
+ check_env
+ log_success "环境初始化完成"
+}
+
+# ============================================
+# 命令行参数解析
+# ============================================
+
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ -h|--host)
+ HOST="$2"
+ shift 2
+ ;;
+ -p|--port)
+ PORT="$2"
+ shift 2
+ ;;
+ -w|--workers)
+ WORKERS="$2"
+ shift 2
+ ;;
+ -r|--reload)
+ RELOAD=true
+ shift
+ ;;
+ -l|--log-level)
+ LOG_LEVEL="$2"
+ shift 2
+ ;;
+ --install)
+ check_python
+ install_dependencies
+ exit 0
+ ;;
+ --init)
+ init_env
+ exit 0
+ ;;
+ --stop)
+ stop_server
+ exit 0
+ ;;
+ --restart)
+ restart_server
+ exit 0
+ ;;
+ --status)
+ show_status
+ exit 0
+ ;;
+ --logs)
+ show_logs "${2:-}"
+ exit 0
+ ;;
+ --help)
+ show_help
+ exit 0
+ ;;
+ *)
+ log_error "未知参数: $1"
+ show_help
+ exit 1
+ ;;
+ esac
+done
+
+# 默认启动
+check_python
+check_env
+start_server
diff --git a/backend/tests/test_api_crypto.py b/backend/tests/test_api_crypto.py
new file mode 100644
index 00000000..7ad51e22
--- /dev/null
+++ b/backend/tests/test_api_crypto.py
@@ -0,0 +1,50 @@
+import unittest
+
+
+from app.core import api_crypto
+
+
+class ApiCryptoTests(unittest.TestCase):
+ def test_derive_key_is_stable_for_same_input(self):
+ key1 = api_crypto.derive_key(
+ fixed_secret="fixed-secret",
+ user_id="u1",
+ ts=1714300000000,
+ nonce="nonce-1",
+ )
+ key2 = api_crypto.derive_key(
+ fixed_secret="fixed-secret",
+ user_id="u1",
+ ts=1714300000000,
+ nonce="nonce-1",
+ )
+ self.assertEqual(key1, key2)
+ self.assertEqual(len(key1), 32)
+
+ def test_encrypt_decrypt_roundtrip(self):
+ key = api_crypto.derive_key(
+ fixed_secret="fixed-secret",
+ user_id="u1",
+ ts=1714300000000,
+ nonce="nonce-1",
+ )
+ payload = {"a": 1, "b": "x"}
+ cipher = api_crypto.encrypt_json(payload, key)
+ plain = api_crypto.decrypt_json(cipher, key)
+ self.assertEqual(plain, payload)
+
+ def test_encrypt_decrypt_roundtrip_cbc(self):
+ key = api_crypto.derive_key(
+ fixed_secret="fixed-secret",
+ user_id="u1",
+ ts=1714300000000,
+ nonce="nonce-2",
+ )
+ payload = {"list": [1, 2, 3], "ok": True}
+ cipher = api_crypto.encrypt_json(payload, key, alg="cbc")
+ plain = api_crypto.decrypt_json(cipher, key, alg="cbc")
+ self.assertEqual(plain, payload)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/backend/venv/bin/Activate.ps1 b/backend/venv/bin/Activate.ps1
new file mode 100644
index 00000000..a3bc6fb1
--- /dev/null
+++ b/backend/venv/bin/Activate.ps1
@@ -0,0 +1,241 @@
+<#
+.Synopsis
+Activate a Python virtual environment for the current PowerShell session.
+
+.Description
+Pushes the python executable for a virtual environment to the front of the
+$Env:PATH environment variable and sets the prompt to signify that you are
+in a Python virtual environment. Makes use of the command line switches as
+well as the `pyvenv.cfg` file values present in the virtual environment.
+
+.Parameter VenvDir
+Path to the directory that contains the virtual environment to activate. The
+default value for this is the parent of the directory that the Activate.ps1
+script is located within.
+
+.Parameter Prompt
+The prompt prefix to display when this virtual environment is activated. By
+default, this prompt is the name of the virtual environment folder (VenvDir)
+surrounded by parentheses and followed by a single space (ie. '(.venv) ').
+
+.Example
+Activate.ps1
+Activates the Python virtual environment that contains the Activate.ps1 script.
+
+.Example
+Activate.ps1 -Verbose
+Activates the Python virtual environment that contains the Activate.ps1 script,
+and shows extra information about the activation as it executes.
+
+.Example
+Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
+Activates the Python virtual environment located in the specified location.
+
+.Example
+Activate.ps1 -Prompt "MyPython"
+Activates the Python virtual environment that contains the Activate.ps1 script,
+and prefixes the current prompt with the specified string (surrounded in
+parentheses) while the virtual environment is active.
+
+.Notes
+On Windows, it may be required to enable this Activate.ps1 script by setting the
+execution policy for the user. You can do this by issuing the following PowerShell
+command:
+
+PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
+
+For more information on Execution Policies:
+https://go.microsoft.com/fwlink/?LinkID=135170
+
+#>
+Param(
+ [Parameter(Mandatory = $false)]
+ [String]
+ $VenvDir,
+ [Parameter(Mandatory = $false)]
+ [String]
+ $Prompt
+)
+
+<# Function declarations --------------------------------------------------- #>
+
+<#
+.Synopsis
+Remove all shell session elements added by the Activate script, including the
+addition of the virtual environment's Python executable from the beginning of
+the PATH variable.
+
+.Parameter NonDestructive
+If present, do not remove this function from the global namespace for the
+session.
+
+#>
+function global:deactivate ([switch]$NonDestructive) {
+ # Revert to original values
+
+ # The prior prompt:
+ if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
+ Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
+ Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
+ }
+
+ # The prior PYTHONHOME:
+ if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
+ Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
+ Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
+ }
+
+ # The prior PATH:
+ if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
+ Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
+ Remove-Item -Path Env:_OLD_VIRTUAL_PATH
+ }
+
+ # Just remove the VIRTUAL_ENV altogether:
+ if (Test-Path -Path Env:VIRTUAL_ENV) {
+ Remove-Item -Path env:VIRTUAL_ENV
+ }
+
+ # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
+ if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
+ Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
+ }
+
+ # Leave deactivate function in the global namespace if requested:
+ if (-not $NonDestructive) {
+ Remove-Item -Path function:deactivate
+ }
+}
+
+<#
+.Description
+Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
+given folder, and returns them in a map.
+
+For each line in the pyvenv.cfg file, if that line can be parsed into exactly
+two strings separated by `=` (with any amount of whitespace surrounding the =)
+then it is considered a `key = value` line. The left hand string is the key,
+the right hand is the value.
+
+If the value starts with a `'` or a `"` then the first and last character is
+stripped from the value before being captured.
+
+.Parameter ConfigDir
+Path to the directory that contains the `pyvenv.cfg` file.
+#>
+function Get-PyVenvConfig(
+ [String]
+ $ConfigDir
+) {
+ Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
+
+ # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
+ $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
+
+ # An empty map will be returned if no config file is found.
+ $pyvenvConfig = @{ }
+
+ if ($pyvenvConfigPath) {
+
+ Write-Verbose "File exists, parse `key = value` lines"
+ $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
+
+ $pyvenvConfigContent | ForEach-Object {
+ $keyval = $PSItem -split "\s*=\s*", 2
+ if ($keyval[0] -and $keyval[1]) {
+ $val = $keyval[1]
+
+ # Remove extraneous quotations around a string value.
+ if ("'""".Contains($val.Substring(0, 1))) {
+ $val = $val.Substring(1, $val.Length - 2)
+ }
+
+ $pyvenvConfig[$keyval[0]] = $val
+ Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
+ }
+ }
+ }
+ return $pyvenvConfig
+}
+
+
+<# Begin Activate script --------------------------------------------------- #>
+
+# Determine the containing directory of this script
+$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
+$VenvExecDir = Get-Item -Path $VenvExecPath
+
+Write-Verbose "Activation script is located in path: '$VenvExecPath'"
+Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
+Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
+
+# Set values required in priority: CmdLine, ConfigFile, Default
+# First, get the location of the virtual environment, it might not be
+# VenvExecDir if specified on the command line.
+if ($VenvDir) {
+ Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
+}
+else {
+ Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
+ $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
+ Write-Verbose "VenvDir=$VenvDir"
+}
+
+# Next, read the `pyvenv.cfg` file to determine any required value such
+# as `prompt`.
+$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
+
+# Next, set the prompt from the command line, or the config file, or
+# just use the name of the virtual environment folder.
+if ($Prompt) {
+ Write-Verbose "Prompt specified as argument, using '$Prompt'"
+}
+else {
+ Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
+ if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
+ Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
+ $Prompt = $pyvenvCfg['prompt'];
+ }
+ else {
+ Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)"
+ Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
+ $Prompt = Split-Path -Path $venvDir -Leaf
+ }
+}
+
+Write-Verbose "Prompt = '$Prompt'"
+Write-Verbose "VenvDir='$VenvDir'"
+
+# Deactivate any currently active virtual environment, but leave the
+# deactivate function in place.
+deactivate -nondestructive
+
+# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
+# that there is an activated venv.
+$env:VIRTUAL_ENV = $VenvDir
+
+if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
+
+ Write-Verbose "Setting prompt to '$Prompt'"
+
+ # Set the prompt to include the env name
+ # Make sure _OLD_VIRTUAL_PROMPT is global
+ function global:_OLD_VIRTUAL_PROMPT { "" }
+ Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
+ New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
+
+ function global:prompt {
+ Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
+ _OLD_VIRTUAL_PROMPT
+ }
+}
+
+# Clear PYTHONHOME
+if (Test-Path -Path Env:PYTHONHOME) {
+ Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
+ Remove-Item -Path Env:PYTHONHOME
+}
+
+# Add the venv to the PATH
+Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
+$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
diff --git a/backend/venv/bin/activate b/backend/venv/bin/activate
new file mode 100644
index 00000000..dc5cb7e9
--- /dev/null
+++ b/backend/venv/bin/activate
@@ -0,0 +1,66 @@
+# This file must be used with "source bin/activate" *from bash*
+# you cannot run it directly
+
+deactivate () {
+ # reset old environment variables
+ if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
+ PATH="${_OLD_VIRTUAL_PATH:-}"
+ export PATH
+ unset _OLD_VIRTUAL_PATH
+ fi
+ if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
+ PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
+ export PYTHONHOME
+ unset _OLD_VIRTUAL_PYTHONHOME
+ fi
+
+ # This should detect bash and zsh, which have a hash command that must
+ # be called to get it to forget past commands. Without forgetting
+ # past commands the $PATH changes we made may not be respected
+ if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
+ hash -r 2> /dev/null
+ fi
+
+ if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
+ PS1="${_OLD_VIRTUAL_PS1:-}"
+ export PS1
+ unset _OLD_VIRTUAL_PS1
+ fi
+
+ unset VIRTUAL_ENV
+ if [ ! "${1:-}" = "nondestructive" ] ; then
+ # Self destruct!
+ unset -f deactivate
+ fi
+}
+
+# unset irrelevant variables
+deactivate nondestructive
+
+VIRTUAL_ENV="/Users/wuzilong/work/00code/sqxp/backend/venv"
+export VIRTUAL_ENV
+
+_OLD_VIRTUAL_PATH="$PATH"
+PATH="$VIRTUAL_ENV/bin:$PATH"
+export PATH
+
+# unset PYTHONHOME if set
+# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
+# could use `if (set -u; : $PYTHONHOME) ;` in bash
+if [ -n "${PYTHONHOME:-}" ] ; then
+ _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
+ unset PYTHONHOME
+fi
+
+if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
+ _OLD_VIRTUAL_PS1="${PS1:-}"
+ PS1="(venv) ${PS1:-}"
+ export PS1
+fi
+
+# This should detect bash and zsh, which have a hash command that must
+# be called to get it to forget past commands. Without forgetting
+# past commands the $PATH changes we made may not be respected
+if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
+ hash -r 2> /dev/null
+fi
diff --git a/backend/venv/bin/activate.csh b/backend/venv/bin/activate.csh
new file mode 100644
index 00000000..a8e24998
--- /dev/null
+++ b/backend/venv/bin/activate.csh
@@ -0,0 +1,25 @@
+# This file must be used with "source bin/activate.csh" *from csh*.
+# You cannot run it directly.
+# Created by Davide Di Blasi .
+# Ported to Python 3.3 venv by Andrew Svetlov
+
+alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate'
+
+# Unset irrelevant variables.
+deactivate nondestructive
+
+setenv VIRTUAL_ENV "/Users/wuzilong/work/00code/sqxp/backend/venv"
+
+set _OLD_VIRTUAL_PATH="$PATH"
+setenv PATH "$VIRTUAL_ENV/bin:$PATH"
+
+
+set _OLD_VIRTUAL_PROMPT="$prompt"
+
+if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
+ set prompt = "(venv) $prompt"
+endif
+
+alias pydoc python -m pydoc
+
+rehash
diff --git a/backend/venv/bin/activate.fish b/backend/venv/bin/activate.fish
new file mode 100644
index 00000000..d96f8b96
--- /dev/null
+++ b/backend/venv/bin/activate.fish
@@ -0,0 +1,64 @@
+# This file must be used with "source /bin/activate.fish" *from fish*
+# (https://fishshell.com/); you cannot run it directly.
+
+function deactivate -d "Exit virtual environment and return to normal shell environment"
+ # reset old environment variables
+ if test -n "$_OLD_VIRTUAL_PATH"
+ set -gx PATH $_OLD_VIRTUAL_PATH
+ set -e _OLD_VIRTUAL_PATH
+ end
+ if test -n "$_OLD_VIRTUAL_PYTHONHOME"
+ set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
+ set -e _OLD_VIRTUAL_PYTHONHOME
+ end
+
+ if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
+ functions -e fish_prompt
+ set -e _OLD_FISH_PROMPT_OVERRIDE
+ functions -c _old_fish_prompt fish_prompt
+ functions -e _old_fish_prompt
+ end
+
+ set -e VIRTUAL_ENV
+ if test "$argv[1]" != "nondestructive"
+ # Self-destruct!
+ functions -e deactivate
+ end
+end
+
+# Unset irrelevant variables.
+deactivate nondestructive
+
+set -gx VIRTUAL_ENV "/Users/wuzilong/work/00code/sqxp/backend/venv"
+
+set -gx _OLD_VIRTUAL_PATH $PATH
+set -gx PATH "$VIRTUAL_ENV/bin" $PATH
+
+# Unset PYTHONHOME if set.
+if set -q PYTHONHOME
+ set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
+ set -e PYTHONHOME
+end
+
+if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
+ # fish uses a function instead of an env var to generate the prompt.
+
+ # Save the current fish_prompt function as the function _old_fish_prompt.
+ functions -c fish_prompt _old_fish_prompt
+
+ # With the original prompt function renamed, we can override with our own.
+ function fish_prompt
+ # Save the return status of the last command.
+ set -l old_status $status
+
+ # Output the venv prompt; color taken from the blue of the Python logo.
+ printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal)
+
+ # Restore the return status of the previous command.
+ echo "exit $old_status" | .
+ # Output the original/"old" prompt.
+ _old_fish_prompt
+ end
+
+ set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
+end
diff --git a/backend/venv/bin/alembic b/backend/venv/bin/alembic
new file mode 100755
index 00000000..59ab07b9
--- /dev/null
+++ b/backend/venv/bin/alembic
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from alembic.config import main
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(main())
diff --git a/backend/venv/bin/dotenv b/backend/venv/bin/dotenv
new file mode 100755
index 00000000..5184eefc
--- /dev/null
+++ b/backend/venv/bin/dotenv
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from dotenv.__main__ import cli
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(cli())
diff --git a/backend/venv/bin/httpx b/backend/venv/bin/httpx
new file mode 100755
index 00000000..3fa1ea1a
--- /dev/null
+++ b/backend/venv/bin/httpx
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from httpx import main
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(main())
diff --git a/backend/venv/bin/mako-render b/backend/venv/bin/mako-render
new file mode 100755
index 00000000..6030ffe3
--- /dev/null
+++ b/backend/venv/bin/mako-render
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from mako.cmd import cmdline
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(cmdline())
diff --git a/backend/venv/bin/pip b/backend/venv/bin/pip
new file mode 100755
index 00000000..3b5e86ac
--- /dev/null
+++ b/backend/venv/bin/pip
@@ -0,0 +1,8 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pip._internal.cli.main import main
+if __name__ == '__main__':
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+ sys.exit(main())
diff --git a/backend/venv/bin/pip3 b/backend/venv/bin/pip3
new file mode 100755
index 00000000..3b5e86ac
--- /dev/null
+++ b/backend/venv/bin/pip3
@@ -0,0 +1,8 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pip._internal.cli.main import main
+if __name__ == '__main__':
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+ sys.exit(main())
diff --git a/backend/venv/bin/pip3.9 b/backend/venv/bin/pip3.9
new file mode 100755
index 00000000..3b5e86ac
--- /dev/null
+++ b/backend/venv/bin/pip3.9
@@ -0,0 +1,8 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pip._internal.cli.main import main
+if __name__ == '__main__':
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+ sys.exit(main())
diff --git a/backend/venv/bin/pyrsa-decrypt b/backend/venv/bin/pyrsa-decrypt
new file mode 100755
index 00000000..4ef4e219
--- /dev/null
+++ b/backend/venv/bin/pyrsa-decrypt
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from rsa.cli import decrypt
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(decrypt())
diff --git a/backend/venv/bin/pyrsa-encrypt b/backend/venv/bin/pyrsa-encrypt
new file mode 100755
index 00000000..d13e4b94
--- /dev/null
+++ b/backend/venv/bin/pyrsa-encrypt
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from rsa.cli import encrypt
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(encrypt())
diff --git a/backend/venv/bin/pyrsa-keygen b/backend/venv/bin/pyrsa-keygen
new file mode 100755
index 00000000..85d705d2
--- /dev/null
+++ b/backend/venv/bin/pyrsa-keygen
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from rsa.cli import keygen
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(keygen())
diff --git a/backend/venv/bin/pyrsa-priv2pub b/backend/venv/bin/pyrsa-priv2pub
new file mode 100755
index 00000000..b4984d82
--- /dev/null
+++ b/backend/venv/bin/pyrsa-priv2pub
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from rsa.util import private_to_public
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(private_to_public())
diff --git a/backend/venv/bin/pyrsa-sign b/backend/venv/bin/pyrsa-sign
new file mode 100755
index 00000000..b45baeaa
--- /dev/null
+++ b/backend/venv/bin/pyrsa-sign
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from rsa.cli import sign
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(sign())
diff --git a/backend/venv/bin/pyrsa-verify b/backend/venv/bin/pyrsa-verify
new file mode 100755
index 00000000..2bdc8021
--- /dev/null
+++ b/backend/venv/bin/pyrsa-verify
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from rsa.cli import verify
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(verify())
diff --git a/backend/venv/bin/python b/backend/venv/bin/python
new file mode 120000
index 00000000..b8a0adbb
--- /dev/null
+++ b/backend/venv/bin/python
@@ -0,0 +1 @@
+python3
\ No newline at end of file
diff --git a/backend/venv/bin/python3 b/backend/venv/bin/python3
new file mode 120000
index 00000000..f25545fe
--- /dev/null
+++ b/backend/venv/bin/python3
@@ -0,0 +1 @@
+/Library/Developer/CommandLineTools/usr/bin/python3
\ No newline at end of file
diff --git a/backend/venv/bin/python3.9 b/backend/venv/bin/python3.9
new file mode 120000
index 00000000..b8a0adbb
--- /dev/null
+++ b/backend/venv/bin/python3.9
@@ -0,0 +1 @@
+python3
\ No newline at end of file
diff --git a/backend/venv/bin/uvicorn b/backend/venv/bin/uvicorn
new file mode 100755
index 00000000..c44b535a
--- /dev/null
+++ b/backend/venv/bin/uvicorn
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from uvicorn.main import main
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(main())
diff --git a/backend/venv/bin/watchfiles b/backend/venv/bin/watchfiles
new file mode 100755
index 00000000..993bbe11
--- /dev/null
+++ b/backend/venv/bin/watchfiles
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from watchfiles.cli import cli
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(cli())
diff --git a/backend/venv/bin/websockets b/backend/venv/bin/websockets
new file mode 100755
index 00000000..12d7afbb
--- /dev/null
+++ b/backend/venv/bin/websockets
@@ -0,0 +1,6 @@
+#!/Users/wuzilong/work/00code/sqxp/backend/venv/bin/python3
+import sys
+from websockets.cli import main
+if __name__ == '__main__':
+ sys.argv[0] = sys.argv[0].removesuffix('.exe')
+ sys.exit(main())
diff --git a/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/INSTALLER b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/INSTALLER
new file mode 100644
index 00000000..a1b589e3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/LICENSE.txt b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/LICENSE.txt
new file mode 100644
index 00000000..07806f8a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/LICENSE.txt
@@ -0,0 +1,19 @@
+This is the MIT license: http://www.opensource.org/licenses/mit-license.php
+
+Copyright (c) Alex Grönholm
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this
+software and associated documentation files (the "Software"), to deal in the Software
+without restriction, including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/METADATA b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/METADATA
new file mode 100644
index 00000000..62df97e7
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/METADATA
@@ -0,0 +1,138 @@
+Metadata-Version: 2.1
+Name: APScheduler
+Version: 3.10.4
+Summary: In-process task scheduler with Cron-like capabilities
+Home-page: https://github.com/agronholm/apscheduler
+Author: Alex Grönholm
+Author-email: apscheduler@nextday.fi
+License: MIT
+Keywords: scheduling cron
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Requires-Python: >=3.6
+License-File: LICENSE.txt
+Requires-Dist: six >=1.4.0
+Requires-Dist: pytz
+Requires-Dist: tzlocal !=3.*,>=2.0
+Requires-Dist: importlib-metadata >=3.6.0 ; python_version < "3.8"
+Provides-Extra: doc
+Requires-Dist: sphinx ; extra == 'doc'
+Requires-Dist: sphinx-rtd-theme ; extra == 'doc'
+Provides-Extra: gevent
+Requires-Dist: gevent ; extra == 'gevent'
+Provides-Extra: mongodb
+Requires-Dist: pymongo >=3.0 ; extra == 'mongodb'
+Provides-Extra: redis
+Requires-Dist: redis >=3.0 ; extra == 'redis'
+Provides-Extra: rethinkdb
+Requires-Dist: rethinkdb >=2.4.0 ; extra == 'rethinkdb'
+Provides-Extra: sqlalchemy
+Requires-Dist: sqlalchemy >=1.4 ; extra == 'sqlalchemy'
+Provides-Extra: testing
+Requires-Dist: pytest ; extra == 'testing'
+Requires-Dist: pytest-asyncio ; extra == 'testing'
+Requires-Dist: pytest-cov ; extra == 'testing'
+Requires-Dist: pytest-tornado5 ; extra == 'testing'
+Provides-Extra: tornado
+Requires-Dist: tornado >=4.3 ; extra == 'tornado'
+Provides-Extra: twisted
+Requires-Dist: twisted ; extra == 'twisted'
+Provides-Extra: zookeeper
+Requires-Dist: kazoo ; extra == 'zookeeper'
+
+.. image:: https://github.com/agronholm/apscheduler/workflows/Python%20codeqa/test/badge.svg?branch=3.x
+ :target: https://github.com/agronholm/apscheduler/actions?query=workflow%3A%22Python+codeqa%2Ftest%22+branch%3A3.x
+ :alt: Build Status
+.. image:: https://coveralls.io/repos/github/agronholm/apscheduler/badge.svg?branch=3.x
+ :target: https://coveralls.io/github/agronholm/apscheduler?branch=3.x
+ :alt: Code Coverage
+.. image:: https://readthedocs.org/projects/apscheduler/badge/?version=3.x
+ :target: https://apscheduler.readthedocs.io/en/master/?badge=3.x
+ :alt: Documentation
+
+Advanced Python Scheduler (APScheduler) is a Python library that lets you schedule your Python code
+to be executed later, either just once or periodically. You can add new jobs or remove old ones on
+the fly as you please. If you store your jobs in a database, they will also survive scheduler
+restarts and maintain their state. When the scheduler is restarted, it will then run all the jobs
+it should have run while it was offline [#f1]_.
+
+Among other things, APScheduler can be used as a cross-platform, application specific replacement
+to platform specific schedulers, such as the cron daemon or the Windows task scheduler. Please
+note, however, that APScheduler is **not** a daemon or service itself, nor does it come with any
+command line tools. It is primarily meant to be run inside existing applications. That said,
+APScheduler does provide some building blocks for you to build a scheduler service or to run a
+dedicated scheduler process.
+
+APScheduler has three built-in scheduling systems you can use:
+
+* Cron-style scheduling (with optional start/end times)
+* Interval-based execution (runs jobs on even intervals, with optional start/end times)
+* One-off delayed execution (runs jobs once, on a set date/time)
+
+You can mix and match scheduling systems and the backends where the jobs are stored any way you
+like. Supported backends for storing jobs include:
+
+* Memory
+* `SQLAlchemy `_ (any RDBMS supported by SQLAlchemy works)
+* `MongoDB `_
+* `Redis `_
+* `RethinkDB `_
+* `ZooKeeper `_
+
+APScheduler also integrates with several common Python frameworks, like:
+
+* `asyncio `_ (:pep:`3156`)
+* `gevent `_
+* `Tornado `_
+* `Twisted `_
+* `Qt `_ (using either
+ `PyQt `_ ,
+ `PySide6 `_ ,
+ `PySide2 `_ or
+ `PySide `_)
+
+There are third party solutions for integrating APScheduler with other frameworks:
+
+* `Django `_
+* `Flask `_
+
+
+.. [#f1] The cutoff period for this is also configurable.
+
+
+Documentation
+-------------
+
+Documentation can be found `here `_.
+
+
+Source
+------
+
+The source can be browsed at `Github `_.
+
+
+Reporting bugs
+--------------
+
+A `bug tracker `_ is provided by Github.
+
+
+Getting help
+------------
+
+If you have problems or other questions, you can either:
+
+* Ask in the `apscheduler `_ room on Gitter
+* Ask on the `APScheduler GitHub discussion forum `_, or
+* Ask on `StackOverflow `_ and tag your
+ question with the ``apscheduler`` tag
diff --git a/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/RECORD b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/RECORD
new file mode 100644
index 00000000..817cc88d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/RECORD
@@ -0,0 +1,84 @@
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/events.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/asyncio.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/base_py3.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/debug.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/gevent.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/pool.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/tornado.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/executors/twisted.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/job.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/memory.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/mongodb.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/redis.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/rethinkdb.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/sqlalchemy.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/jobstores/zookeeper.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/asyncio.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/background.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/blocking.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/gevent.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/qt.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/tornado.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/schedulers/twisted.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/combining.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/cron/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/cron/expressions.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/cron/fields.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/date.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/triggers/interval.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/apscheduler/util.cpython-39.pyc,,
+APScheduler-3.10.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+APScheduler-3.10.4.dist-info/LICENSE.txt,sha256=YWP3mH37ONa8MgzitwsvArhivEESZRbVUu8c1DJH51g,1130
+APScheduler-3.10.4.dist-info/METADATA,sha256=ITYjDYv8SBO2ynuPiXmySCDJPjfvrFElLJoKQr58h8U,5695
+APScheduler-3.10.4.dist-info/RECORD,,
+APScheduler-3.10.4.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+APScheduler-3.10.4.dist-info/WHEEL,sha256=5sUXSg9e4bi7lTLOHcm6QEYwO5TIF1TNbTSVFVjcJcc,92
+APScheduler-3.10.4.dist-info/entry_points.txt,sha256=KMxTUp2QykDNL6w-WBU5xrk8ebroCPEBN0eZtyL3x2w,1147
+APScheduler-3.10.4.dist-info/top_level.txt,sha256=O3oMCWxG-AHkecUoO6Ze7-yYjWrttL95uHO8-RFdYvE,12
+apscheduler/__init__.py,sha256=c_KXMg1QziacYqUpDuzLY5g1mcEZvBLq1dJY7NjLoKc,452
+apscheduler/events.py,sha256=KRMTDQUS6d2uVnrQvPoz3ZPV5V9XKsCAZLsgx913FFo,3593
+apscheduler/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+apscheduler/executors/asyncio.py,sha256=9m4wvRHSSYplllxAQyxWkPVcFdyFG5aZbHt5nfWKIAc,1859
+apscheduler/executors/base.py,sha256=hogiMc_t-huw6BMod0HEeY2FhRNmAAUyNNuBHvIX31M,5336
+apscheduler/executors/base_py3.py,sha256=8WOpTeX1NA-spdbEQ1oJMh5T2O_t2UdsaSnAh-iEWe0,1831
+apscheduler/executors/debug.py,sha256=15_ogSBzl8RRCfBYDnkIV2uMH8cLk1KImYmBa_NVGpc,573
+apscheduler/executors/gevent.py,sha256=aulrNmoefyBgrOkH9awRhFiXIDnSCnZ4U0o0_JXIXgc,777
+apscheduler/executors/pool.py,sha256=h4cYgKMRhjpNHmkhlogHLbmT4O_q6HePXVLmiJIHC3c,2484
+apscheduler/executors/tornado.py,sha256=DU75VaQ9R6nBuy8lbPUvDKUgsuJcZqwAvURC5vg3r6w,1780
+apscheduler/executors/twisted.py,sha256=bRoU0C4BoVcS6_BjKD5wfUs0IJpGkmLsRAcMH2rJJss,778
+apscheduler/job.py,sha256=JCRERBpfWLuomPiNNHX-jrluEwfHkdscEmz4i0Y8rao,11216
+apscheduler/jobstores/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+apscheduler/jobstores/base.py,sha256=DXzSW9XscueHZHMvy1qFiG-vYqUl_MMv0n0uBSZWXGo,4523
+apscheduler/jobstores/memory.py,sha256=ZxWiKsqfsCHFvac-6X9BztuhnuSxlOYi1dhT6g-pjQo,3655
+apscheduler/jobstores/mongodb.py,sha256=r9t2neNuzfPuf_omDm0KdkLGPZXLksiH-U3j13MIBlM,5347
+apscheduler/jobstores/redis.py,sha256=kjQDIzPXz-Yq976U9HK3aMkcCI_QRLKgTADQWKewtik,5483
+apscheduler/jobstores/rethinkdb.py,sha256=k1rSLYJqejuhQxJY3pXwHAQYcpZ1QFJsoQ8n0oEu5MM,5863
+apscheduler/jobstores/sqlalchemy.py,sha256=LIA9iSGMvuPTVqGHdztgQs4YFmYN1xqXvpJauYNK470,6529
+apscheduler/jobstores/zookeeper.py,sha256=avGLXaJGjHD0F7uG6rLJ2gg_TXNqXDEM4PqOu56f-Xg,6363
+apscheduler/schedulers/__init__.py,sha256=jM63xA_K7GSToBenhsz-SCcqfhk1pdEVb6ajwoO5Kqg,406
+apscheduler/schedulers/asyncio.py,sha256=iJO6QUo1oW16giOU_nW8WMu2b9NTWT4Tg2gY586G08w,1994
+apscheduler/schedulers/background.py,sha256=751p-f5Di6pY4x6UXlZggpxQ5k2ObJ_Q5wSeWmKHS8o,1566
+apscheduler/schedulers/base.py,sha256=hCchDyhEXCoVmCfGgD3QMrKumYYLAUwY4456tQrukAY,43780
+apscheduler/schedulers/blocking.py,sha256=8nubfJ4PoUnAkEY6WRQG4COzG4SxGyW9PjuVPhDAbsk,985
+apscheduler/schedulers/gevent.py,sha256=csPBvV75FGcboXXsdex6fCD7J54QgBddYNdWj62ZO9g,1031
+apscheduler/schedulers/qt.py,sha256=jy58cP5roWOv68ytg8fiwtxMVnZKw7a8tkCHbLWeUs8,1329
+apscheduler/schedulers/tornado.py,sha256=D9Vaq3Ee9EFiXa1jDy9tedI048gR_YT_LAFUWqO_uEw,1926
+apscheduler/schedulers/twisted.py,sha256=D5EBjjMRtMBxy0_aAURcULAI8Ky2IvCTr9tK9sO1rYk,1844
+apscheduler/triggers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+apscheduler/triggers/base.py,sha256=BvBJdOnIeVClXPXeInzYK25cN64jAc4a9IiEQucSiVk,1355
+apscheduler/triggers/combining.py,sha256=klaSoBp1kyrPX5D3gBpNTlsGKjks5QeKPW5JN_MVs30,3449
+apscheduler/triggers/cron/__init__.py,sha256=D39BQ63qWyk6XZcSuWth46ELQ3VIFpYjUHh7Kj65Z9M,9251
+apscheduler/triggers/cron/expressions.py,sha256=hu1kq0mKvivIw7U0D0Nnrbuk3q01dCuhZ7SHRPw6qhI,9184
+apscheduler/triggers/cron/fields.py,sha256=NWPClh1NgSOpTlJ3sm1TXM_ViC2qJGKWkd_vg0xsw7o,3510
+apscheduler/triggers/date.py,sha256=RrfB1PNO9G9e91p1BOf-y_TseVHQQR-KJPhNdPpAHcU,1705
+apscheduler/triggers/interval.py,sha256=ABjcZFaGYAAgdAaUQIuLr9_dLszIifu88qaXrJmdxQ4,4377
+apscheduler/util.py,sha256=aCLu_v8-c7rpY6sD7EKgxH2zYjZARiBdqKFZktaxO68,13260
diff --git a/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/REQUESTED b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/REQUESTED
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/WHEEL b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/WHEEL
new file mode 100644
index 00000000..2c08da08
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.41.1)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/entry_points.txt b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/entry_points.txt
new file mode 100644
index 00000000..0adfe3ea
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/entry_points.txt
@@ -0,0 +1,23 @@
+[apscheduler.executors]
+asyncio = apscheduler.executors.asyncio:AsyncIOExecutor [asyncio]
+debug = apscheduler.executors.debug:DebugExecutor
+gevent = apscheduler.executors.gevent:GeventExecutor [gevent]
+processpool = apscheduler.executors.pool:ProcessPoolExecutor
+threadpool = apscheduler.executors.pool:ThreadPoolExecutor
+tornado = apscheduler.executors.tornado:TornadoExecutor [tornado]
+twisted = apscheduler.executors.twisted:TwistedExecutor [twisted]
+
+[apscheduler.jobstores]
+memory = apscheduler.jobstores.memory:MemoryJobStore
+mongodb = apscheduler.jobstores.mongodb:MongoDBJobStore [mongodb]
+redis = apscheduler.jobstores.redis:RedisJobStore [redis]
+rethinkdb = apscheduler.jobstores.rethinkdb:RethinkDBJobStore [rethinkdb]
+sqlalchemy = apscheduler.jobstores.sqlalchemy:SQLAlchemyJobStore [sqlalchemy]
+zookeeper = apscheduler.jobstores.zookeeper:ZooKeeperJobStore [zookeeper]
+
+[apscheduler.triggers]
+and = apscheduler.triggers.combining:AndTrigger
+cron = apscheduler.triggers.cron:CronTrigger
+date = apscheduler.triggers.date:DateTrigger
+interval = apscheduler.triggers.interval:IntervalTrigger
+or = apscheduler.triggers.combining:OrTrigger
diff --git a/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/top_level.txt b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/top_level.txt
new file mode 100644
index 00000000..d31d10dd
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/APScheduler-3.10.4.dist-info/top_level.txt
@@ -0,0 +1 @@
+apscheduler
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/AES.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/AES.py
new file mode 100644
index 00000000..52ac33da
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/AES.py
@@ -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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/AES.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/AES.pyi
new file mode 100644
index 00000000..125e3e24
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/AES.pyi
@@ -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]
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC2.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC2.py
new file mode 100644
index 00000000..0ba7e335
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC2.py
@@ -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) `
+:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) `
+:var MODE_CFB: :ref:`Cipher FeedBack (CFB) `
+:var MODE_OFB: :ref:`Output FeedBack (OFB) `
+:var MODE_CTR: :ref:`CounTer Mode (CTR) `
+:var MODE_OPENPGP: :ref:`OpenPGP Mode `
+:var MODE_EAX: :ref:`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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC2.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC2.pyi
new file mode 100644
index 00000000..771c6dc8
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC2.pyi
@@ -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]
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC4.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC4.py
new file mode 100644
index 00000000..97dc2297
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC4.py
@@ -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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC4.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC4.pyi
new file mode 100644
index 00000000..b0815859
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ARC4.pyi
@@ -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]
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Blowfish.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Blowfish.py
new file mode 100644
index 00000000..6005ffe2
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Blowfish.py
@@ -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) `
+:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) `
+:var MODE_CFB: :ref:`Cipher FeedBack (CFB) `
+:var MODE_OFB: :ref:`Output FeedBack (OFB) `
+:var MODE_CTR: :ref:`CounTer Mode (CTR) `
+:var MODE_OPENPGP: :ref:`OpenPGP Mode `
+:var MODE_EAX: :ref:`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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Blowfish.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Blowfish.pyi
new file mode 100644
index 00000000..76c3808c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Blowfish.pyi
@@ -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]
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/CAST.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/CAST.py
new file mode 100644
index 00000000..c7e82c1c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/CAST.py
@@ -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) `
+:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) `
+:var MODE_CFB: :ref:`Cipher FeedBack (CFB) `
+:var MODE_OFB: :ref:`Output FeedBack (OFB) `
+:var MODE_CTR: :ref:`CounTer Mode (CTR) `
+:var MODE_OPENPGP: :ref:`OpenPGP Mode `
+:var MODE_EAX: :ref:`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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/CAST.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/CAST.pyi
new file mode 100644
index 00000000..f107552d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/CAST.pyi
@@ -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]
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20.py
new file mode 100644
index 00000000..f77f1bbe
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20.py
@@ -0,0 +1,291 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20.pyi
new file mode 100644
index 00000000..f5001cdb
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20.pyi
@@ -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
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20_Poly1305.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20_Poly1305.py
new file mode 100644
index 00000000..2027b7e9
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20_Poly1305.py
@@ -0,0 +1,334 @@
+# ===================================================================
+#
+# Copyright (c) 2018, Helder Eijs
+# 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
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20_Poly1305.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20_Poly1305.pyi
new file mode 100644
index 00000000..109e8051
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/ChaCha20_Poly1305.pyi
@@ -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
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES.py
new file mode 100644
index 00000000..5cc286ae
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES.py
@@ -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) `
+:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) `
+:var MODE_CFB: :ref:`Cipher FeedBack (CFB) `
+:var MODE_OFB: :ref:`Output FeedBack (OFB) `
+:var MODE_CTR: :ref:`CounTer Mode (CTR) `
+:var MODE_OPENPGP: :ref:`OpenPGP Mode `
+:var MODE_EAX: :ref:`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
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES.pyi
new file mode 100644
index 00000000..dbcb5d25
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES.pyi
@@ -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
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES3.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES3.py
new file mode 100644
index 00000000..c0d93671
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES3.py
@@ -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) `
+:var MODE_CBC: :ref:`Cipher-Block Chaining (CBC) `
+:var MODE_CFB: :ref:`Cipher FeedBack (CFB) `
+:var MODE_OFB: :ref:`Output FeedBack (OFB) `
+:var MODE_CTR: :ref:`CounTer Mode (CTR) `
+:var MODE_OPENPGP: :ref:`OpenPGP Mode `
+:var MODE_EAX: :ref:`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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES3.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES3.pyi
new file mode 100644
index 00000000..d4dd0bb2
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/DES3.pyi
@@ -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]
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_OAEP.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_OAEP.py
new file mode 100644
index 00000000..e87487e0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_OAEP.py
@@ -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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_OAEP.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_OAEP.pyi
new file mode 100644
index 00000000..6cb80da6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_OAEP.pyi
@@ -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: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_v1_5.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_v1_5.py
new file mode 100644
index 00000000..ce30be61
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_v1_5.py
@@ -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
+ `_.
+
+ :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
+ `_.
+
+ 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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_v1_5.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_v1_5.pyi
new file mode 100644
index 00000000..1719f01a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/PKCS1_v1_5.pyi
@@ -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: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Salsa20.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Salsa20.py
new file mode 100644
index 00000000..62d0b292
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Salsa20.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+#
+# Cipher/Salsa20.py : Salsa20 stream cipher (http://cr.yp.to/snuffle.html)
+#
+# Contributed by Fabrizio Tarizzo .
+#
+# ===================================================================
+# 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)
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Salsa20.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Salsa20.pyi
new file mode 100644
index 00000000..cf8690e2
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/Salsa20.pyi
@@ -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]
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_ARC4.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_ARC4.abi3.so
new file mode 100755
index 00000000..f1bd5efe
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_ARC4.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_EKSBlowfish.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_EKSBlowfish.py
new file mode 100644
index 00000000..a844fae4
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_EKSBlowfish.py
@@ -0,0 +1,131 @@
+# ===================================================================
+#
+# Copyright (c) 2019, Legrandin
+# 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 `_.
+ 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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_EKSBlowfish.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_EKSBlowfish.pyi
new file mode 100644
index 00000000..95db3794
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_EKSBlowfish.pyi
@@ -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]
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_Salsa20.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_Salsa20.abi3.so
new file mode 100755
index 00000000..402ea16f
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_Salsa20.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/__init__.py
new file mode 100644
index 00000000..57709232
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/__init__.py
@@ -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
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/__init__.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/__init__.pyi
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_chacha20.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_chacha20.abi3.so
new file mode 100755
index 00000000..7e70185b
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_chacha20.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cbc.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cbc.py
new file mode 100644
index 00000000..36c78efe
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cbc.py
@@ -0,0 +1,293 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cbc.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cbc.pyi
new file mode 100644
index 00000000..8b9fb16d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cbc.pyi
@@ -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: ...
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ccm.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ccm.py
new file mode 100644
index 00000000..e4ffd0d5
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ccm.py
@@ -0,0 +1,671 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ccm.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ccm.pyi
new file mode 100644
index 00000000..98af96a4
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ccm.pyi
@@ -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: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cfb.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cfb.py
new file mode 100644
index 00000000..482e44cb
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cfb.py
@@ -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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cfb.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cfb.pyi
new file mode 100644
index 00000000..e13a909b
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_cfb.pyi
@@ -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: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ctr.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ctr.py
new file mode 100644
index 00000000..81f2be9c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ctr.py
@@ -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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ctr.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ctr.pyi
new file mode 100644
index 00000000..ce708557
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ctr.pyi
@@ -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: ...
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_eax.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_eax.py
new file mode 100644
index 00000000..62cf4d8b
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_eax.py
@@ -0,0 +1,408 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_eax.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_eax.pyi
new file mode 100644
index 00000000..cbfa467e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_eax.pyi
@@ -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: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ecb.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ecb.py
new file mode 100644
index 00000000..37833572
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ecb.py
@@ -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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ecb.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ecb.pyi
new file mode 100644
index 00000000..1772b23c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ecb.pyi
@@ -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: ...
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_gcm.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_gcm.py
new file mode 100644
index 00000000..05195107
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_gcm.py
@@ -0,0 +1,620 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_gcm.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_gcm.pyi
new file mode 100644
index 00000000..8912955f
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_gcm.pyi
@@ -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: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_kw.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_kw.py
new file mode 100644
index 00000000..9248982d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_kw.py
@@ -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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_kwp.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_kwp.py
new file mode 100644
index 00000000..e6cac94c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_kwp.py
@@ -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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ocb.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ocb.py
new file mode 100644
index 00000000..a271ca12
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ocb.py
@@ -0,0 +1,532 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ocb.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ocb.pyi
new file mode 100644
index 00000000..a1909fc3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ocb.pyi
@@ -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: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ofb.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ofb.py
new file mode 100644
index 00000000..1a29b056
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ofb.py
@@ -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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ofb.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ofb.pyi
new file mode 100644
index 00000000..60f7f004
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_ofb.pyi
@@ -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: ...
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_openpgp.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_openpgp.py
new file mode 100644
index 00000000..d079d591
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_openpgp.py
@@ -0,0 +1,206 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_openpgp.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_openpgp.pyi
new file mode 100644
index 00000000..14b81058
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_openpgp.pyi
@@ -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: ...
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_siv.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_siv.py
new file mode 100644
index 00000000..1805ec09
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_siv.py
@@ -0,0 +1,392 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_siv.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_siv.pyi
new file mode 100644
index 00000000..2934f23d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_mode_siv.pyi
@@ -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: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_pkcs1_decode.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_pkcs1_decode.abi3.so
new file mode 100755
index 00000000..817ab005
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_pkcs1_decode.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_pkcs1_oaep_decode.py b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_pkcs1_oaep_decode.py
new file mode 100644
index 00000000..fc075282
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_pkcs1_oaep_decode.py
@@ -0,0 +1,41 @@
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, c_size_t,
+ c_uint8_ptr)
+
+
+_raw_pkcs1_decode = load_pycryptodome_raw_lib("Crypto.Cipher._pkcs1_decode",
+ """
+ int pkcs1_decode(const uint8_t *em, size_t len_em,
+ const uint8_t *sentinel, size_t len_sentinel,
+ size_t expected_pt_len,
+ uint8_t *output);
+
+ int oaep_decode(const uint8_t *em,
+ size_t em_len,
+ const uint8_t *lHash,
+ size_t hLen,
+ const uint8_t *db,
+ size_t db_len);
+ """)
+
+
+def pkcs1_decode(em, sentinel, expected_pt_len, output):
+ if len(em) != len(output):
+ raise ValueError("Incorrect output length")
+
+ ret = _raw_pkcs1_decode.pkcs1_decode(c_uint8_ptr(em),
+ c_size_t(len(em)),
+ c_uint8_ptr(sentinel),
+ c_size_t(len(sentinel)),
+ c_size_t(expected_pt_len),
+ c_uint8_ptr(output))
+ return ret
+
+
+def oaep_decode(em, lHash, db):
+ ret = _raw_pkcs1_decode.oaep_decode(c_uint8_ptr(em),
+ c_size_t(len(em)),
+ c_uint8_ptr(lHash),
+ c_size_t(len(lHash)),
+ c_uint8_ptr(db),
+ c_size_t(len(db)))
+ return ret
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_aes.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_aes.abi3.so
new file mode 100755
index 00000000..78f0cbd5
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_aes.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_arc2.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_arc2.abi3.so
new file mode 100755
index 00000000..0f021dbb
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_arc2.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_blowfish.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_blowfish.abi3.so
new file mode 100755
index 00000000..ebfd6177
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_blowfish.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_cast.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_cast.abi3.so
new file mode 100755
index 00000000..0de30e08
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_cast.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_cbc.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_cbc.abi3.so
new file mode 100755
index 00000000..8f5d96a3
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_cbc.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_cfb.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_cfb.abi3.so
new file mode 100755
index 00000000..26226589
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_cfb.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ctr.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ctr.abi3.so
new file mode 100755
index 00000000..e1b9708c
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ctr.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_des.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_des.abi3.so
new file mode 100755
index 00000000..b302873d
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_des.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_des3.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_des3.abi3.so
new file mode 100755
index 00000000..b533653a
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_des3.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ecb.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ecb.abi3.so
new file mode 100755
index 00000000..be589c25
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ecb.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_eksblowfish.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_eksblowfish.abi3.so
new file mode 100755
index 00000000..29a575aa
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_eksblowfish.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ocb.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ocb.abi3.so
new file mode 100755
index 00000000..1b2f4f4c
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ocb.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ofb.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ofb.abi3.so
new file mode 100755
index 00000000..0b495d02
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Cipher/_raw_ofb.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2b.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2b.py
new file mode 100644
index 00000000..e3ec46e6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2b.py
@@ -0,0 +1,247 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.Util.py3compat import bord, tobytes
+
+from Crypto.Random import get_random_bytes
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_blake2b_lib = load_pycryptodome_raw_lib("Crypto.Hash._BLAKE2b",
+ """
+ int blake2b_init(void **state,
+ const uint8_t *key,
+ size_t key_size,
+ size_t digest_size);
+ int blake2b_destroy(void *state);
+ int blake2b_update(void *state,
+ const uint8_t *buf,
+ size_t len);
+ int blake2b_digest(const void *state,
+ uint8_t digest[64]);
+ int blake2b_copy(const void *src, void *dst);
+ """)
+
+
+class BLAKE2b_Hash(object):
+ """A BLAKE2b hash object.
+ Do not instantiate directly. Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar block_size: the size in bytes of the internal message block,
+ input to the compression function
+ :vartype block_size: integer
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The internal block size of the hash algorithm in bytes.
+ block_size = 64
+
+ def __init__(self, data, key, digest_bytes, update_after_digest):
+
+ # The size of the resulting hash in bytes.
+ self.digest_size = digest_bytes
+
+ self._update_after_digest = update_after_digest
+ self._digest_done = False
+
+ # See https://tools.ietf.org/html/rfc7693
+ if digest_bytes in (20, 32, 48, 64) and not key:
+ self.oid = "1.3.6.1.4.1.1722.12.2.1." + str(digest_bytes)
+
+ state = VoidPointer()
+ result = _raw_blake2b_lib.blake2b_init(state.address_of(),
+ c_uint8_ptr(key),
+ c_size_t(len(key)),
+ c_size_t(digest_bytes)
+ )
+ if result:
+ raise ValueError("Error %d while instantiating BLAKE2b" % result)
+ self._state = SmartPointer(state.get(),
+ _raw_blake2b_lib.blake2b_destroy)
+ if data:
+ self.update(data)
+
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (bytes/bytearray/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._digest_done and not self._update_after_digest:
+ raise TypeError("You can only call 'digest' or 'hexdigest' on this object")
+
+ result = _raw_blake2b_lib.blake2b_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while hashing BLAKE2b data" % result)
+ return self
+
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bfr = create_string_buffer(64)
+ result = _raw_blake2b_lib.blake2b_digest(self._state.get(),
+ bfr)
+ if result:
+ raise ValueError("Error %d while creating BLAKE2b digest" % result)
+
+ self._digest_done = True
+
+ return get_raw_buffer(bfr)[:self.digest_size]
+
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in tuple(self.digest())])
+
+
+ def verify(self, mac_tag):
+ """Verify that a given **binary** MAC (computed by another party)
+ is valid.
+
+ Args:
+ mac_tag (bytes/bytearray/memoryview): the expected MAC of the message.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ secret = get_random_bytes(16)
+
+ mac1 = new(digest_bits=160, key=secret, data=mac_tag)
+ mac2 = new(digest_bits=160, key=secret, data=self.digest())
+
+ if mac1.digest() != mac2.digest():
+ raise ValueError("MAC check failed")
+
+
+ def hexverify(self, hex_mac_tag):
+ """Verify that a given **printable** MAC (computed by another party)
+ is valid.
+
+ Args:
+ hex_mac_tag (string): the expected MAC of the message, as a hexadecimal string.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ self.verify(unhexlify(tobytes(hex_mac_tag)))
+
+
+ def new(self, **kwargs):
+ """Return a new instance of a BLAKE2b hash object.
+ See :func:`new`.
+ """
+
+ if "digest_bytes" not in kwargs and "digest_bits" not in kwargs:
+ kwargs["digest_bytes"] = self.digest_size
+
+ return new(**kwargs)
+
+
+def new(**kwargs):
+ """Create a new hash object.
+
+ Args:
+ data (bytes/bytearray/memoryview):
+ Optional. The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`BLAKE2b_Hash.update`.
+ digest_bytes (integer):
+ Optional. The size of the digest, in bytes (1 to 64). Default is 64.
+ digest_bits (integer):
+ Optional and alternative to ``digest_bytes``.
+ The size of the digest, in bits (8 to 512, in steps of 8).
+ Default is 512.
+ key (bytes/bytearray/memoryview):
+ Optional. The key to use to compute the MAC (1 to 64 bytes).
+ If not specified, no key will be used.
+ update_after_digest (boolean):
+ Optional. By default, a hash object cannot be updated anymore after
+ the digest is computed. When this flag is ``True``, such check
+ is no longer enforced.
+
+ Returns:
+ A :class:`BLAKE2b_Hash` hash object
+ """
+
+ data = kwargs.pop("data", None)
+ update_after_digest = kwargs.pop("update_after_digest", False)
+
+ digest_bytes = kwargs.pop("digest_bytes", None)
+ digest_bits = kwargs.pop("digest_bits", None)
+ if None not in (digest_bytes, digest_bits):
+ raise TypeError("Only one digest parameter must be provided")
+ if (None, None) == (digest_bytes, digest_bits):
+ digest_bytes = 64
+ if digest_bytes is not None:
+ if not (1 <= digest_bytes <= 64):
+ raise ValueError("'digest_bytes' not in range 1..64")
+ else:
+ if not (8 <= digest_bits <= 512) or (digest_bits % 8):
+ raise ValueError("'digest_bits' not in range 8..512, "
+ "with steps of 8")
+ digest_bytes = digest_bits // 8
+
+ key = kwargs.pop("key", b"")
+ if len(key) > 64:
+ raise ValueError("BLAKE2b key cannot exceed 64 bytes")
+
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ return BLAKE2b_Hash(data, key, digest_bytes, update_after_digest)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2b.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2b.pyi
new file mode 100644
index 00000000..d37c3749
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2b.pyi
@@ -0,0 +1,32 @@
+from typing import Any, Union
+from types import ModuleType
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class BLAKE2b_Hash(object):
+ block_size: int
+ digest_size: int
+ oid: str
+
+ def __init__(self,
+ data: Buffer,
+ key: Buffer,
+ digest_bytes: bytes,
+ update_after_digest: bool) -> None: ...
+ def update(self, data: Buffer) -> BLAKE2b_Hash: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def verify(self, mac_tag: Buffer) -> None: ...
+ def hexverify(self, hex_mac_tag: str) -> None: ...
+ def new(self,
+ data: Buffer = ...,
+ digest_bytes: int = ...,
+ digest_bits: int = ...,
+ key: Buffer = ...,
+ update_after_digest: bool = ...) -> BLAKE2b_Hash: ...
+
+def new(data: Buffer = ...,
+ digest_bytes: int = ...,
+ digest_bits: int = ...,
+ key: Buffer = ...,
+ update_after_digest: bool = ...) -> BLAKE2b_Hash: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2s.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2s.py
new file mode 100644
index 00000000..192d1459
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2s.py
@@ -0,0 +1,247 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.Util.py3compat import bord, tobytes
+
+from Crypto.Random import get_random_bytes
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_blake2s_lib = load_pycryptodome_raw_lib("Crypto.Hash._BLAKE2s",
+ """
+ int blake2s_init(void **state,
+ const uint8_t *key,
+ size_t key_size,
+ size_t digest_size);
+ int blake2s_destroy(void *state);
+ int blake2s_update(void *state,
+ const uint8_t *buf,
+ size_t len);
+ int blake2s_digest(const void *state,
+ uint8_t digest[32]);
+ int blake2s_copy(const void *src, void *dst);
+ """)
+
+
+class BLAKE2s_Hash(object):
+ """A BLAKE2s hash object.
+ Do not instantiate directly. Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar block_size: the size in bytes of the internal message block,
+ input to the compression function
+ :vartype block_size: integer
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The internal block size of the hash algorithm in bytes.
+ block_size = 32
+
+ def __init__(self, data, key, digest_bytes, update_after_digest):
+
+ # The size of the resulting hash in bytes.
+ self.digest_size = digest_bytes
+
+ self._update_after_digest = update_after_digest
+ self._digest_done = False
+
+ # See https://tools.ietf.org/html/rfc7693
+ if digest_bytes in (16, 20, 28, 32) and not key:
+ self.oid = "1.3.6.1.4.1.1722.12.2.2." + str(digest_bytes)
+
+ state = VoidPointer()
+ result = _raw_blake2s_lib.blake2s_init(state.address_of(),
+ c_uint8_ptr(key),
+ c_size_t(len(key)),
+ c_size_t(digest_bytes)
+ )
+ if result:
+ raise ValueError("Error %d while instantiating BLAKE2s" % result)
+ self._state = SmartPointer(state.get(),
+ _raw_blake2s_lib.blake2s_destroy)
+ if data:
+ self.update(data)
+
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._digest_done and not self._update_after_digest:
+ raise TypeError("You can only call 'digest' or 'hexdigest' on this object")
+
+ result = _raw_blake2s_lib.blake2s_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while hashing BLAKE2s data" % result)
+ return self
+
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bfr = create_string_buffer(32)
+ result = _raw_blake2s_lib.blake2s_digest(self._state.get(),
+ bfr)
+ if result:
+ raise ValueError("Error %d while creating BLAKE2s digest" % result)
+
+ self._digest_done = True
+
+ return get_raw_buffer(bfr)[:self.digest_size]
+
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in tuple(self.digest())])
+
+
+ def verify(self, mac_tag):
+ """Verify that a given **binary** MAC (computed by another party)
+ is valid.
+
+ Args:
+ mac_tag (byte string/byte array/memoryview): the expected MAC of the message.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ secret = get_random_bytes(16)
+
+ mac1 = new(digest_bits=160, key=secret, data=mac_tag)
+ mac2 = new(digest_bits=160, key=secret, data=self.digest())
+
+ if mac1.digest() != mac2.digest():
+ raise ValueError("MAC check failed")
+
+
+ def hexverify(self, hex_mac_tag):
+ """Verify that a given **printable** MAC (computed by another party)
+ is valid.
+
+ Args:
+ hex_mac_tag (string): the expected MAC of the message, as a hexadecimal string.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ self.verify(unhexlify(tobytes(hex_mac_tag)))
+
+
+ def new(self, **kwargs):
+ """Return a new instance of a BLAKE2s hash object.
+ See :func:`new`.
+ """
+
+ if "digest_bytes" not in kwargs and "digest_bits" not in kwargs:
+ kwargs["digest_bytes"] = self.digest_size
+
+ return new(**kwargs)
+
+
+def new(**kwargs):
+ """Create a new hash object.
+
+ Args:
+ data (byte string/byte array/memoryview):
+ Optional. The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`BLAKE2s_Hash.update`.
+ digest_bytes (integer):
+ Optional. The size of the digest, in bytes (1 to 32). Default is 32.
+ digest_bits (integer):
+ Optional and alternative to ``digest_bytes``.
+ The size of the digest, in bits (8 to 256, in steps of 8).
+ Default is 256.
+ key (byte string):
+ Optional. The key to use to compute the MAC (1 to 64 bytes).
+ If not specified, no key will be used.
+ update_after_digest (boolean):
+ Optional. By default, a hash object cannot be updated anymore after
+ the digest is computed. When this flag is ``True``, such check
+ is no longer enforced.
+
+ Returns:
+ A :class:`BLAKE2s_Hash` hash object
+ """
+
+ data = kwargs.pop("data", None)
+ update_after_digest = kwargs.pop("update_after_digest", False)
+
+ digest_bytes = kwargs.pop("digest_bytes", None)
+ digest_bits = kwargs.pop("digest_bits", None)
+ if None not in (digest_bytes, digest_bits):
+ raise TypeError("Only one digest parameter must be provided")
+ if (None, None) == (digest_bytes, digest_bits):
+ digest_bytes = 32
+ if digest_bytes is not None:
+ if not (1 <= digest_bytes <= 32):
+ raise ValueError("'digest_bytes' not in range 1..32")
+ else:
+ if not (8 <= digest_bits <= 256) or (digest_bits % 8):
+ raise ValueError("'digest_bits' not in range 8..256, "
+ "with steps of 8")
+ digest_bytes = digest_bits // 8
+
+ key = kwargs.pop("key", b"")
+ if len(key) > 32:
+ raise ValueError("BLAKE2s key cannot exceed 32 bytes")
+
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ return BLAKE2s_Hash(data, key, digest_bytes, update_after_digest)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2s.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2s.pyi
new file mode 100644
index 00000000..374b3a43
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/BLAKE2s.pyi
@@ -0,0 +1,26 @@
+from typing import Any, Union
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class BLAKE2s_Hash(object):
+ block_size: int
+ digest_size: int
+ oid: str
+
+ def __init__(self,
+ data: Buffer,
+ key: Buffer,
+ digest_bytes: bytes,
+ update_after_digest: bool) -> None: ...
+ def update(self, data: Buffer) -> BLAKE2s_Hash: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def verify(self, mac_tag: Buffer) -> None: ...
+ def hexverify(self, hex_mac_tag: str) -> None: ...
+ def new(self, **kwargs: Any) -> BLAKE2s_Hash: ...
+
+def new(data: Buffer = ...,
+ digest_bytes: int = ...,
+ digest_bits: int = ...,
+ key: Buffer = ...,
+ update_after_digest: bool = ...) -> BLAKE2s_Hash: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/CMAC.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/CMAC.py
new file mode 100644
index 00000000..764be861
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/CMAC.py
@@ -0,0 +1,306 @@
+# -*- coding: utf-8 -*-
+#
+# Hash/CMAC.py - Implements the CMAC algorithm
+#
+# ===================================================================
+# 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 binascii import unhexlify
+
+from Crypto.Hash import BLAKE2s
+from Crypto.Util.strxor import strxor
+from Crypto.Util.number import long_to_bytes, bytes_to_long
+from Crypto.Util.py3compat import bord, tobytes, _copy_bytes
+from Crypto.Random import get_random_bytes
+
+
+# The size of the authentication tag produced by the MAC.
+digest_size = None
+
+
+def _shift_bytes(bs, xor_lsb=0):
+ num = (bytes_to_long(bs) << 1) ^ xor_lsb
+ return long_to_bytes(num, len(bs))[-len(bs):]
+
+
+class CMAC(object):
+ """A CMAC hash object.
+ Do not instantiate directly. Use the :func:`new` function.
+
+ :ivar digest_size: the size in bytes of the resulting MAC tag
+ :vartype digest_size: integer
+ """
+
+ digest_size = None
+
+ def __init__(self, key, msg, ciphermod, cipher_params, mac_len,
+ update_after_digest):
+
+ self.digest_size = mac_len
+
+ self._key = _copy_bytes(None, None, key)
+ self._factory = ciphermod
+ self._cipher_params = cipher_params
+ self._block_size = bs = ciphermod.block_size
+ self._mac_tag = None
+ self._update_after_digest = update_after_digest
+
+ # Section 5.3 of NIST SP 800 38B and Appendix B
+ if bs == 8:
+ const_Rb = 0x1B
+ self._max_size = 8 * (2 ** 21)
+ elif bs == 16:
+ const_Rb = 0x87
+ self._max_size = 16 * (2 ** 48)
+ else:
+ raise TypeError("CMAC requires a cipher with a block size"
+ " of 8 or 16 bytes, not %d" % bs)
+
+ # Compute sub-keys
+ zero_block = b'\x00' * bs
+ self._ecb = ciphermod.new(key,
+ ciphermod.MODE_ECB,
+ **self._cipher_params)
+ L = self._ecb.encrypt(zero_block)
+ if bord(L[0]) & 0x80:
+ self._k1 = _shift_bytes(L, const_Rb)
+ else:
+ self._k1 = _shift_bytes(L)
+ if bord(self._k1[0]) & 0x80:
+ self._k2 = _shift_bytes(self._k1, const_Rb)
+ else:
+ self._k2 = _shift_bytes(self._k1)
+
+ # Initialize CBC cipher with zero IV
+ self._cbc = ciphermod.new(key,
+ ciphermod.MODE_CBC,
+ zero_block,
+ **self._cipher_params)
+
+ # Cache for outstanding data to authenticate
+ self._cache = bytearray(bs)
+ self._cache_n = 0
+
+ # Last piece of ciphertext produced
+ self._last_ct = zero_block
+
+ # Last block that was encrypted with AES
+ self._last_pt = None
+
+ # Counter for total message size
+ self._data_size = 0
+
+ if msg:
+ self.update(msg)
+
+ def update(self, msg):
+ """Authenticate the next chunk of message.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of data
+ """
+
+ if self._mac_tag is not None and not self._update_after_digest:
+ raise TypeError("update() cannot be called after digest() or verify()")
+
+ self._data_size += len(msg)
+ bs = self._block_size
+
+ if self._cache_n > 0:
+ filler = min(bs - self._cache_n, len(msg))
+ self._cache[self._cache_n:self._cache_n+filler] = msg[:filler]
+ self._cache_n += filler
+
+ if self._cache_n < bs:
+ return self
+
+ msg = memoryview(msg)[filler:]
+ self._update(self._cache)
+ self._cache_n = 0
+
+ remain = len(msg) % bs
+ if remain > 0:
+ self._update(msg[:-remain])
+ self._cache[:remain] = msg[-remain:]
+ else:
+ self._update(msg)
+ self._cache_n = remain
+ return self
+
+ def _update(self, data_block):
+ """Update a block aligned to the block boundary"""
+
+ bs = self._block_size
+ assert len(data_block) % bs == 0
+
+ if len(data_block) == 0:
+ return
+
+ ct = self._cbc.encrypt(data_block)
+ if len(data_block) == bs:
+ second_last = self._last_ct
+ else:
+ second_last = ct[-bs*2:-bs]
+ self._last_ct = ct[-bs:]
+ self._last_pt = strxor(second_last, data_block[-bs:])
+
+ def copy(self):
+ """Return a copy ("clone") of the CMAC object.
+
+ The copy will have the same internal state as the original CMAC
+ object.
+ This can be used to efficiently compute the MAC tag of byte
+ strings that share a common initial substring.
+
+ :return: An :class:`CMAC`
+ """
+
+ obj = self.__new__(CMAC)
+ obj.__dict__ = self.__dict__.copy()
+ obj._cbc = self._factory.new(self._key,
+ self._factory.MODE_CBC,
+ self._last_ct,
+ **self._cipher_params)
+ obj._cache = self._cache[:]
+ obj._last_ct = self._last_ct[:]
+ return obj
+
+ def digest(self):
+ """Return the **binary** (non-printable) MAC tag of the message
+ that has been authenticated so far.
+
+ :return: The MAC tag, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bs = self._block_size
+
+ if self._mac_tag is not None and not self._update_after_digest:
+ return self._mac_tag
+
+ if self._data_size > self._max_size:
+ raise ValueError("MAC is unsafe for this message")
+
+ if self._cache_n == 0 and self._data_size > 0:
+ # Last block was full
+ pt = strxor(self._last_pt, self._k1)
+ else:
+ # Last block is partial (or message length is zero)
+ partial = self._cache[:]
+ partial[self._cache_n:] = b'\x80' + b'\x00' * (bs - self._cache_n - 1)
+ pt = strxor(strxor(self._last_ct, partial), self._k2)
+
+ self._mac_tag = self._ecb.encrypt(pt)[:self.digest_size]
+
+ return self._mac_tag
+
+ def hexdigest(self):
+ """Return the **printable** MAC tag of the message authenticated so far.
+
+ :return: The MAC tag, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x)
+ for x in tuple(self.digest())])
+
+ def verify(self, mac_tag):
+ """Verify that a given **binary** MAC (computed by another party)
+ is valid.
+
+ Args:
+ mac_tag (byte string/byte array/memoryview): the expected MAC of the message.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ secret = get_random_bytes(16)
+
+ mac1 = BLAKE2s.new(digest_bits=160, key=secret, data=mac_tag)
+ mac2 = BLAKE2s.new(digest_bits=160, key=secret, data=self.digest())
+
+ if mac1.digest() != mac2.digest():
+ raise ValueError("MAC check failed")
+
+ def hexverify(self, hex_mac_tag):
+ """Verify that a given **printable** MAC (computed by another party)
+ is valid.
+
+ Args:
+ hex_mac_tag (string): the expected MAC of the message, as a hexadecimal string.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ self.verify(unhexlify(tobytes(hex_mac_tag)))
+
+
+def new(key, msg=None, ciphermod=None, cipher_params=None, mac_len=None,
+ update_after_digest=False):
+ """Create a new MAC object.
+
+ Args:
+ key (byte string/byte array/memoryview):
+ key for the CMAC object.
+ The key must be valid for the underlying cipher algorithm.
+ For instance, it must be 16 bytes long for AES-128.
+ ciphermod (module):
+ A cipher module from :mod:`Crypto.Cipher`.
+ The cipher's block size has to be 128 bits,
+ like :mod:`Crypto.Cipher.AES`, to reduce the probability
+ of collisions.
+ msg (byte string/byte array/memoryview):
+ Optional. The very first chunk of the message to authenticate.
+ It is equivalent to an early call to `CMAC.update`. Optional.
+ cipher_params (dict):
+ Optional. A set of parameters to use when instantiating a cipher
+ object.
+ mac_len (integer):
+ Length of the MAC, in bytes.
+ It must be at least 4 bytes long.
+ The default (and recommended) length matches the size of a cipher block.
+ update_after_digest (boolean):
+ Optional. By default, a hash object cannot be updated anymore after
+ the digest is computed. When this flag is ``True``, such check
+ is no longer enforced.
+ Returns:
+ A :class:`CMAC` object
+ """
+
+ if ciphermod is None:
+ raise TypeError("ciphermod must be specified (try AES)")
+
+ cipher_params = {} if cipher_params is None else dict(cipher_params)
+
+ if mac_len is None:
+ mac_len = ciphermod.block_size
+
+ if mac_len < 4:
+ raise ValueError("MAC tag length must be at least 4 bytes long")
+
+ if mac_len > ciphermod.block_size:
+ raise ValueError("MAC tag length cannot be larger than a cipher block (%d) bytes" % ciphermod.block_size)
+
+ return CMAC(key, msg, ciphermod, cipher_params, mac_len,
+ update_after_digest)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/CMAC.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/CMAC.pyi
new file mode 100644
index 00000000..acdf0553
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/CMAC.pyi
@@ -0,0 +1,30 @@
+from types import ModuleType
+from typing import Union, Dict, Any
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+digest_size: int
+
+class CMAC(object):
+ digest_size: int
+
+ def __init__(self,
+ key: Buffer,
+ msg: Buffer,
+ ciphermod: ModuleType,
+ cipher_params: Dict[str, Any],
+ mac_len: int, update_after_digest: bool) -> None: ...
+ def update(self, data: Buffer) -> CMAC: ...
+ def copy(self) -> CMAC: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def verify(self, mac_tag: Buffer) -> None: ...
+ def hexverify(self, hex_mac_tag: str) -> None: ...
+
+
+def new(key: Buffer,
+ msg: Buffer = ...,
+ ciphermod: ModuleType = ...,
+ cipher_params: Dict[str, Any] = ...,
+ mac_len: int = ...,
+ update_after_digest: bool = ...) -> CMAC: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/HMAC.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/HMAC.py
new file mode 100644
index 00000000..7124d3e1
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/HMAC.py
@@ -0,0 +1,238 @@
+#
+# HMAC.py - Implements the HMAC algorithm as described by RFC 2104.
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.Util.py3compat import bord, tobytes
+
+from binascii import unhexlify
+
+from Crypto.Hash import BLAKE2s
+from Crypto.Util.strxor import strxor
+from Crypto.Random import get_random_bytes
+
+__all__ = ['new', 'HMAC']
+
+_hash2hmac_oid = {
+ '1.3.14.3.2.26': '1.2.840.113549.2.7', # SHA-1
+ '2.16.840.1.101.3.4.2.4': '1.2.840.113549.2.8', # SHA-224
+ '2.16.840.1.101.3.4.2.1': '1.2.840.113549.2.9', # SHA-256
+ '2.16.840.1.101.3.4.2.2': '1.2.840.113549.2.10', # SHA-384
+ '2.16.840.1.101.3.4.2.3': '1.2.840.113549.2.11', # SHA-512
+ '2.16.840.1.101.3.4.2.5': '1.2.840.113549.2.12', # SHA-512_224
+ '2.16.840.1.101.3.4.2.6': '1.2.840.113549.2.13', # SHA-512_256
+ '2.16.840.1.101.3.4.2.7': '2.16.840.1.101.3.4.2.13', # SHA-3 224
+ '2.16.840.1.101.3.4.2.8': '2.16.840.1.101.3.4.2.14', # SHA-3 256
+ '2.16.840.1.101.3.4.2.9': '2.16.840.1.101.3.4.2.15', # SHA-3 384
+ '2.16.840.1.101.3.4.2.10': '2.16.840.1.101.3.4.2.16', # SHA-3 512
+}
+
+_hmac2hash_oid = {v: k for k, v in _hash2hmac_oid.items()}
+
+
+class HMAC(object):
+ """An HMAC hash object.
+ Do not instantiate directly. Use the :func:`new` function.
+
+ :ivar digest_size: the size in bytes of the resulting MAC tag
+ :vartype digest_size: integer
+
+ :ivar oid: the ASN.1 object ID of the HMAC algorithm.
+ Only present if the algorithm was officially assigned one.
+ """
+
+ def __init__(self, key, msg=b"", digestmod=None):
+
+ if digestmod is None:
+ from Crypto.Hash import MD5
+ digestmod = MD5
+
+ if msg is None:
+ msg = b""
+
+ # Size of the MAC tag
+ self.digest_size = digestmod.digest_size
+
+ self._digestmod = digestmod
+
+ # Hash OID --> HMAC OID
+ try:
+ self.oid = _hash2hmac_oid[digestmod.oid]
+ except (KeyError, AttributeError):
+ pass
+
+ if isinstance(key, memoryview):
+ key = key.tobytes()
+
+ try:
+ if len(key) <= digestmod.block_size:
+ # Step 1 or 2
+ key_0 = key + b"\x00" * (digestmod.block_size - len(key))
+ else:
+ # Step 3
+ hash_k = digestmod.new(key).digest()
+ key_0 = hash_k + b"\x00" * (digestmod.block_size - len(hash_k))
+ except AttributeError:
+ # Not all hash types have "block_size"
+ raise ValueError("Hash type incompatible to HMAC")
+
+ # Step 4
+ key_0_ipad = strxor(key_0, b"\x36" * len(key_0))
+
+ # Start step 5 and 6
+ self._inner = digestmod.new(key_0_ipad)
+ self._inner.update(msg)
+
+ # Step 7
+ key_0_opad = strxor(key_0, b"\x5c" * len(key_0))
+
+ # Start step 8 and 9
+ self._outer = digestmod.new(key_0_opad)
+
+ def update(self, msg):
+ """Authenticate the next chunk of message.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of data
+ """
+
+ self._inner.update(msg)
+ return self
+
+ def _pbkdf2_hmac_assist(self, first_digest, iterations):
+ """Carry out the expensive inner loop for PBKDF2-HMAC"""
+
+ result = self._digestmod._pbkdf2_hmac_assist(
+ self._inner,
+ self._outer,
+ first_digest,
+ iterations)
+ return result
+
+ def copy(self):
+ """Return a copy ("clone") of the HMAC object.
+
+ The copy will have the same internal state as the original HMAC
+ object.
+ This can be used to efficiently compute the MAC tag of byte
+ strings that share a common initial substring.
+
+ :return: An :class:`HMAC`
+ """
+
+ new_hmac = HMAC(b"fake key", digestmod=self._digestmod)
+
+ # Syncronize the state
+ new_hmac._inner = self._inner.copy()
+ new_hmac._outer = self._outer.copy()
+
+ return new_hmac
+
+ def digest(self):
+ """Return the **binary** (non-printable) MAC tag of the message
+ authenticated so far.
+
+ :return: The MAC tag digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ frozen_outer_hash = self._outer.copy()
+ frozen_outer_hash.update(self._inner.digest())
+ return frozen_outer_hash.digest()
+
+ def verify(self, mac_tag):
+ """Verify that a given **binary** MAC (computed by another party)
+ is valid.
+
+ Args:
+ mac_tag (byte string/byte string/memoryview): the expected MAC of the message.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ secret = get_random_bytes(16)
+
+ mac1 = BLAKE2s.new(digest_bits=160, key=secret, data=mac_tag)
+ mac2 = BLAKE2s.new(digest_bits=160, key=secret, data=self.digest())
+
+ if mac1.digest() != mac2.digest():
+ raise ValueError("MAC check failed")
+
+ def hexdigest(self):
+ """Return the **printable** MAC tag of the message authenticated so far.
+
+ :return: The MAC tag, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x)
+ for x in tuple(self.digest())])
+
+ def hexverify(self, hex_mac_tag):
+ """Verify that a given **printable** MAC (computed by another party)
+ is valid.
+
+ Args:
+ hex_mac_tag (string): the expected MAC of the message,
+ as a hexadecimal string.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ self.verify(unhexlify(tobytes(hex_mac_tag)))
+
+
+def new(key, msg=b"", digestmod=None):
+ """Create a new MAC object.
+
+ Args:
+ key (bytes/bytearray/memoryview):
+ key for the MAC object.
+ It must be long enough to match the expected security level of the
+ MAC.
+ msg (bytes/bytearray/memoryview):
+ Optional. The very first chunk of the message to authenticate.
+ It is equivalent to an early call to :meth:`HMAC.update`.
+ digestmod (module):
+ The hash to use to implement the HMAC.
+ Default is :mod:`Crypto.Hash.MD5`.
+
+ Returns:
+ An :class:`HMAC` object
+ """
+
+ return HMAC(key, msg, digestmod)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/HMAC.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/HMAC.pyi
new file mode 100644
index 00000000..b577230f
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/HMAC.pyi
@@ -0,0 +1,25 @@
+from types import ModuleType
+from typing import Union, Dict
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+digest_size: int
+
+class HMAC(object):
+ digest_size: int
+
+ def __init__(self,
+ key: Buffer,
+ msg: Buffer,
+ digestmod: ModuleType) -> None: ...
+ def update(self, msg: Buffer) -> HMAC: ...
+ def copy(self) -> HMAC: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def verify(self, mac_tag: Buffer) -> None: ...
+ def hexverify(self, hex_mac_tag: str) -> None: ...
+
+
+def new(key: Buffer,
+ msg: Buffer = ...,
+ digestmod: ModuleType = ...) -> HMAC: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC128.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC128.py
new file mode 100644
index 00000000..05061fc2
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC128.py
@@ -0,0 +1,179 @@
+# ===================================================================
+#
+# Copyright (c) 2021, Legrandin
+# 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.Util.py3compat import bord, tobytes, is_bytes
+from Crypto.Random import get_random_bytes
+
+from . import cSHAKE128, SHA3_256
+from .cSHAKE128 import _bytepad, _encode_str, _right_encode
+
+
+class KMAC_Hash(object):
+ """A KMAC hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+ """
+
+ def __init__(self, data, key, mac_len, custom,
+ oid_variant, cshake, rate):
+
+ # See https://tools.ietf.org/html/rfc8702
+ self.oid = "2.16.840.1.101.3.4.2." + oid_variant
+ self.digest_size = mac_len
+
+ self._mac = None
+
+ partial_newX = _bytepad(_encode_str(tobytes(key)), rate)
+ self._cshake = cshake._new(partial_newX, custom, b"KMAC")
+
+ if data:
+ self._cshake.update(data)
+
+ def update(self, data):
+ """Authenticate the next chunk of message.
+
+ Args:
+ data (bytes/bytearray/memoryview): The next chunk of the message to
+ authenticate.
+ """
+
+ if self._mac:
+ raise TypeError("You can only call 'digest' or 'hexdigest' on this object")
+
+ self._cshake.update(data)
+ return self
+
+ def digest(self):
+ """Return the **binary** (non-printable) MAC tag of the message.
+
+ :return: The MAC tag. Binary form.
+ :rtype: byte string
+ """
+
+ if not self._mac:
+ self._cshake.update(_right_encode(self.digest_size * 8))
+ self._mac = self._cshake.read(self.digest_size)
+
+ return self._mac
+
+ def hexdigest(self):
+ """Return the **printable** MAC tag of the message.
+
+ :return: The MAC tag. Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in tuple(self.digest())])
+
+ def verify(self, mac_tag):
+ """Verify that a given **binary** MAC (computed by another party)
+ is valid.
+
+ Args:
+ mac_tag (bytes/bytearray/memoryview): the expected MAC of the message.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ secret = get_random_bytes(16)
+
+ mac1 = SHA3_256.new(secret + mac_tag)
+ mac2 = SHA3_256.new(secret + self.digest())
+
+ if mac1.digest() != mac2.digest():
+ raise ValueError("MAC check failed")
+
+ def hexverify(self, hex_mac_tag):
+ """Verify that a given **printable** MAC (computed by another party)
+ is valid.
+
+ Args:
+ hex_mac_tag (string): the expected MAC of the message, as a hexadecimal string.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ self.verify(unhexlify(tobytes(hex_mac_tag)))
+
+ def new(self, **kwargs):
+ """Return a new instance of a KMAC hash object.
+ See :func:`new`.
+ """
+
+ if "mac_len" not in kwargs:
+ kwargs["mac_len"] = self.digest_size
+
+ return new(**kwargs)
+
+
+def new(**kwargs):
+ """Create a new KMAC128 object.
+
+ Args:
+ key (bytes/bytearray/memoryview):
+ The key to use to compute the MAC.
+ It must be at least 128 bits long (16 bytes).
+ data (bytes/bytearray/memoryview):
+ Optional. The very first chunk of the message to authenticate.
+ It is equivalent to an early call to :meth:`KMAC_Hash.update`.
+ mac_len (integer):
+ Optional. The size of the authentication tag, in bytes.
+ Default is 64. Minimum is 8.
+ custom (bytes/bytearray/memoryview):
+ Optional. A customization byte string (``S`` in SP 800-185).
+
+ Returns:
+ A :class:`KMAC_Hash` hash object
+ """
+
+ key = kwargs.pop("key", None)
+ if not is_bytes(key):
+ raise TypeError("You must pass a key to KMAC128")
+ if len(key) < 16:
+ raise ValueError("The key must be at least 128 bits long (16 bytes)")
+
+ data = kwargs.pop("data", None)
+
+ mac_len = kwargs.pop("mac_len", 64)
+ if mac_len < 8:
+ raise ValueError("'mac_len' must be 8 bytes or more")
+
+ custom = kwargs.pop("custom", b"")
+
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ return KMAC_Hash(data, key, mac_len, custom, "19", cSHAKE128, 168)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC128.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC128.pyi
new file mode 100644
index 00000000..8947dabc
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC128.pyi
@@ -0,0 +1,33 @@
+from typing import Union
+from types import ModuleType
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class KMAC_Hash(object):
+
+ def __init__(self,
+ data: Buffer,
+ key: Buffer,
+ mac_len: int,
+ custom: Buffer,
+ oid_variant: str,
+ cshake: ModuleType,
+ rate: int) -> None: ...
+
+ def update(self, data: Buffer) -> KMAC_Hash: ...
+
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def verify(self, mac_tag: Buffer) -> None: ...
+ def hexverify(self, hex_mac_tag: str) -> None: ...
+ def new(self,
+ data: Buffer = ...,
+ mac_len: int = ...,
+ key: Buffer = ...,
+ custom: Buffer = ...) -> KMAC_Hash: ...
+
+
+def new(key: Buffer,
+ data: Buffer = ...,
+ mac_len: int = ...,
+ custom: Buffer = ...) -> KMAC_Hash: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC256.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC256.py
new file mode 100644
index 00000000..2be8e2f3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC256.py
@@ -0,0 +1,74 @@
+# ===================================================================
+#
+# Copyright (c) 2021, Legrandin
+# 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.Util.py3compat import is_bytes
+
+from .KMAC128 import KMAC_Hash
+from . import cSHAKE256
+
+
+def new(**kwargs):
+ """Create a new KMAC256 object.
+
+ Args:
+ key (bytes/bytearray/memoryview):
+ The key to use to compute the MAC.
+ It must be at least 256 bits long (32 bytes).
+ data (bytes/bytearray/memoryview):
+ Optional. The very first chunk of the message to authenticate.
+ It is equivalent to an early call to :meth:`KMAC_Hash.update`.
+ mac_len (integer):
+ Optional. The size of the authentication tag, in bytes.
+ Default is 64. Minimum is 8.
+ custom (bytes/bytearray/memoryview):
+ Optional. A customization byte string (``S`` in SP 800-185).
+
+ Returns:
+ A :class:`KMAC_Hash` hash object
+ """
+
+ key = kwargs.pop("key", None)
+ if not is_bytes(key):
+ raise TypeError("You must pass a key to KMAC256")
+ if len(key) < 32:
+ raise ValueError("The key must be at least 256 bits long (32 bytes)")
+
+ data = kwargs.pop("data", None)
+
+ mac_len = kwargs.pop("mac_len", 64)
+ if mac_len < 8:
+ raise ValueError("'mac_len' must be 8 bytes or more")
+
+ custom = kwargs.pop("custom", b"")
+
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ return KMAC_Hash(data, key, mac_len, custom, "20", cSHAKE256, 136)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC256.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC256.pyi
new file mode 100644
index 00000000..86cc5000
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KMAC256.pyi
@@ -0,0 +1,10 @@
+from typing import Union
+
+from .KMAC128 import KMAC_Hash
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+def new(key: Buffer,
+ data: Buffer = ...,
+ mac_len: int = ...,
+ custom: Buffer = ...) -> KMAC_Hash: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KangarooTwelve.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KangarooTwelve.py
new file mode 100644
index 00000000..5cf68579
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KangarooTwelve.py
@@ -0,0 +1,222 @@
+# ===================================================================
+#
+# Copyright (c) 2021, Legrandin
+# 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.Util.number import long_to_bytes
+from Crypto.Util.py3compat import bchr
+
+from . import TurboSHAKE128
+
+def _length_encode(x):
+ if x == 0:
+ return b'\x00'
+
+ S = long_to_bytes(x)
+ return S + bchr(len(S))
+
+
+# Possible states for a KangarooTwelve instance, which depend on the amount of data processed so far.
+SHORT_MSG = 1 # Still within the first 8192 bytes, but it is not certain we will exceed them.
+LONG_MSG_S0 = 2 # Still within the first 8192 bytes, and it is certain we will exceed them.
+LONG_MSG_SX = 3 # Beyond the first 8192 bytes.
+SQUEEZING = 4 # No more data to process.
+
+
+class K12_XOF(object):
+ """A KangarooTwelve hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+ """
+
+ def __init__(self, data, custom):
+
+ if custom == None:
+ custom = b''
+
+ self._custom = custom + _length_encode(len(custom))
+ self._state = SHORT_MSG
+ self._padding = None # Final padding is only decided in read()
+
+ # Internal hash that consumes FinalNode
+ # The real domain separation byte will be known before squeezing
+ self._hash1 = TurboSHAKE128.new(domain=1)
+ self._length1 = 0
+
+ # Internal hash that produces CV_i (reset each time)
+ self._hash2 = None
+ self._length2 = 0
+
+ # Incremented by one for each 8192-byte block
+ self._ctr = 0
+
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Hash the next piece of data.
+
+ .. note::
+ For better performance, submit chunks with a length multiple of 8192 bytes.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the
+ message to hash.
+ """
+
+ if self._state == SQUEEZING:
+ raise TypeError("You cannot call 'update' after the first 'read'")
+
+ if self._state == SHORT_MSG:
+ next_length = self._length1 + len(data)
+
+ if next_length + len(self._custom) <= 8192:
+ self._length1 = next_length
+ self._hash1.update(data)
+ return self
+
+ # Switch to tree hashing
+ self._state = LONG_MSG_S0
+
+ if self._state == LONG_MSG_S0:
+ data_mem = memoryview(data)
+ assert(self._length1 < 8192)
+ dtc = min(len(data), 8192 - self._length1)
+ self._hash1.update(data_mem[:dtc])
+ self._length1 += dtc
+
+ if self._length1 < 8192:
+ return self
+
+ # Finish hashing S_0 and start S_1
+ assert(self._length1 == 8192)
+
+ divider = b'\x03' + b'\x00' * 7
+ self._hash1.update(divider)
+ self._length1 += 8
+
+ self._hash2 = TurboSHAKE128.new(domain=0x0B)
+ self._length2 = 0
+ self._ctr = 1
+
+ self._state = LONG_MSG_SX
+ return self.update(data_mem[dtc:])
+
+ # LONG_MSG_SX
+ assert(self._state == LONG_MSG_SX)
+ index = 0
+ len_data = len(data)
+
+ # All iteractions could actually run in parallel
+ data_mem = memoryview(data)
+ while index < len_data:
+
+ new_index = min(index + 8192 - self._length2, len_data)
+ self._hash2.update(data_mem[index:new_index])
+ self._length2 += new_index - index
+ index = new_index
+
+ if self._length2 == 8192:
+ cv_i = self._hash2.read(32)
+ self._hash1.update(cv_i)
+ self._length1 += 32
+ self._hash2._reset()
+ self._length2 = 0
+ self._ctr += 1
+
+ return self
+
+ def read(self, length):
+ """
+ Produce more bytes of the digest.
+
+ .. note::
+ You cannot use :meth:`update` anymore after the first call to
+ :meth:`read`.
+
+ Args:
+ length (integer): the amount of bytes this method must return
+
+ :return: the next piece of XOF output (of the given length)
+ :rtype: byte string
+ """
+
+ custom_was_consumed = False
+
+ if self._state == SHORT_MSG:
+ self._hash1.update(self._custom)
+ self._padding = 0x07
+ self._state = SQUEEZING
+
+ if self._state == LONG_MSG_S0:
+ self.update(self._custom)
+ custom_was_consumed = True
+ assert(self._state == LONG_MSG_SX)
+
+ if self._state == LONG_MSG_SX:
+ if not custom_was_consumed:
+ self.update(self._custom)
+
+ # Is there still some leftover data in hash2?
+ if self._length2 > 0:
+ cv_i = self._hash2.read(32)
+ self._hash1.update(cv_i)
+ self._length1 += 32
+ self._hash2._reset()
+ self._length2 = 0
+ self._ctr += 1
+
+ trailer = _length_encode(self._ctr - 1) + b'\xFF\xFF'
+ self._hash1.update(trailer)
+
+ self._padding = 0x06
+ self._state = SQUEEZING
+
+ self._hash1._domain = self._padding
+ return self._hash1.read(length)
+
+ def new(self, data=None, custom=b''):
+ return type(self)(data, custom)
+
+
+def new(data=None, custom=None):
+ """Return a fresh instance of a KangarooTwelve object.
+
+ Args:
+ data (bytes/bytearray/memoryview):
+ Optional.
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+ custom (bytes):
+ Optional.
+ A customization byte string.
+
+ :Return: A :class:`K12_XOF` object
+ """
+
+ return K12_XOF(data, custom)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KangarooTwelve.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KangarooTwelve.pyi
new file mode 100644
index 00000000..8b3fd74f
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/KangarooTwelve.pyi
@@ -0,0 +1,16 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class K12_XOF(object):
+ def __init__(self,
+ data: Optional[Buffer] = ...,
+ custom: Optional[bytes] = ...) -> None: ...
+ def update(self, data: Buffer) -> K12_XOF: ...
+ def read(self, length: int) -> bytes: ...
+ def new(self,
+ data: Optional[Buffer] = ...,
+ custom: Optional[bytes] = ...) -> None: ...
+
+def new(data: Optional[Buffer] = ...,
+ custom: Optional[Buffer] = ...) -> K12_XOF: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD2.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD2.py
new file mode 100644
index 00000000..41decbb6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD2.py
@@ -0,0 +1,166 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.Util.py3compat import bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_md2_lib = load_pycryptodome_raw_lib(
+ "Crypto.Hash._MD2",
+ """
+ int md2_init(void **shaState);
+ int md2_destroy(void *shaState);
+ int md2_update(void *hs,
+ const uint8_t *buf,
+ size_t len);
+ int md2_digest(const void *shaState,
+ uint8_t digest[20]);
+ int md2_copy(const void *src, void *dst);
+ """)
+
+
+class MD2Hash(object):
+ """An MD2 hash object.
+ Do not instantiate directly. Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar block_size: the size in bytes of the internal message block,
+ input to the compression function
+ :vartype block_size: integer
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 16
+ # The internal block size of the hash algorithm in bytes.
+ block_size = 16
+ # ASN.1 Object ID
+ oid = "1.2.840.113549.2.2"
+
+ def __init__(self, data=None):
+ state = VoidPointer()
+ result = _raw_md2_lib.md2_init(state.address_of())
+ if result:
+ raise ValueError("Error %d while instantiating MD2"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_md2_lib.md2_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ result = _raw_md2_lib.md2_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while instantiating MD2"
+ % result)
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_md2_lib.md2_digest(self._state.get(),
+ bfr)
+ if result:
+ raise ValueError("Error %d while instantiating MD2"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = MD2Hash()
+ result = _raw_md2_lib.md2_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying MD2" % result)
+ return clone
+
+ def new(self, data=None):
+ return MD2Hash(data)
+
+
+def new(data=None):
+ """Create a new hash object.
+
+ :parameter data:
+ Optional. The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`MD2Hash.update`.
+ :type data: bytes/bytearray/memoryview
+
+ :Return: A :class:`MD2Hash` hash object
+ """
+
+ return MD2Hash().new(data)
+
+# The size of the resulting hash in bytes.
+digest_size = MD2Hash.digest_size
+
+# The internal block size of the hash algorithm in bytes.
+block_size = MD2Hash.block_size
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD2.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD2.pyi
new file mode 100644
index 00000000..95a97a9e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD2.pyi
@@ -0,0 +1,19 @@
+from typing import Union
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class MD4Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+
+ def __init__(self, data: Buffer = ...) -> None: ...
+ def update(self, data: Buffer) -> None: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> MD4Hash: ...
+ def new(self, data: Buffer = ...) -> MD4Hash: ...
+
+def new(data: Buffer = ...) -> MD4Hash: ...
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD4.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD4.py
new file mode 100644
index 00000000..be12b192
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD4.py
@@ -0,0 +1,185 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""
+MD4 is specified in RFC1320_ and produces the 128 bit digest of a message.
+
+ >>> from Crypto.Hash import MD4
+ >>>
+ >>> h = MD4.new()
+ >>> h.update(b'Hello')
+ >>> print h.hexdigest()
+
+MD4 stand for Message Digest version 4, and it was invented by Rivest in 1990.
+This algorithm is insecure. Do not use it for new designs.
+
+.. _RFC1320: http://tools.ietf.org/html/rfc1320
+"""
+
+from Crypto.Util.py3compat import bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_md4_lib = load_pycryptodome_raw_lib(
+ "Crypto.Hash._MD4",
+ """
+ int md4_init(void **shaState);
+ int md4_destroy(void *shaState);
+ int md4_update(void *hs,
+ const uint8_t *buf,
+ size_t len);
+ int md4_digest(const void *shaState,
+ uint8_t digest[20]);
+ int md4_copy(const void *src, void *dst);
+ """)
+
+
+class MD4Hash(object):
+ """Class that implements an MD4 hash
+ """
+
+ #: The size of the resulting hash in bytes.
+ digest_size = 16
+ #: The internal block size of the hash algorithm in bytes.
+ block_size = 64
+ #: ASN.1 Object ID
+ oid = "1.2.840.113549.2.4"
+
+ def __init__(self, data=None):
+ state = VoidPointer()
+ result = _raw_md4_lib.md4_init(state.address_of())
+ if result:
+ raise ValueError("Error %d while instantiating MD4"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_md4_lib.md4_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Repeated calls are equivalent to a single call with the concatenation
+ of all the arguments. In other words:
+
+ >>> m.update(a); m.update(b)
+
+ is equivalent to:
+
+ >>> m.update(a+b)
+
+ :Parameters:
+ data : byte string/byte array/memoryview
+ The next chunk of the message being hashed.
+ """
+
+ result = _raw_md4_lib.md4_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while instantiating MD4"
+ % result)
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that
+ has been hashed so far.
+
+ This method does not change the state of the hash object.
+ You can continue updating the object after calling this function.
+
+ :Return: A byte string of `digest_size` bytes. It may contain non-ASCII
+ characters, including null bytes.
+ """
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_md4_lib.md4_digest(self._state.get(),
+ bfr)
+ if result:
+ raise ValueError("Error %d while instantiating MD4"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been
+ hashed so far.
+
+ This method does not change the state of the hash object.
+
+ :Return: A string of 2* `digest_size` characters. It contains only
+ hexadecimal ASCII digits.
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :Return: A hash object of the same type
+ """
+
+ clone = MD4Hash()
+ result = _raw_md4_lib.md4_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying MD4" % result)
+ return clone
+
+ def new(self, data=None):
+ return MD4Hash(data)
+
+
+def new(data=None):
+ """Return a fresh instance of the hash object.
+
+ :Parameters:
+ data : byte string/byte array/memoryview
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to `MD4Hash.update()`.
+ Optional.
+
+ :Return: A `MD4Hash` object
+ """
+ return MD4Hash().new(data)
+
+#: The size of the resulting hash in bytes.
+digest_size = MD4Hash.digest_size
+
+#: The internal block size of the hash algorithm in bytes.
+block_size = MD4Hash.block_size
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD4.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD4.pyi
new file mode 100644
index 00000000..a9a72953
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD4.pyi
@@ -0,0 +1,19 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class MD4Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+
+ def __init__(self, data: Optional[Buffer] = ...) -> None: ...
+ def update(self, data: Buffer) -> None: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> MD4Hash: ...
+ def new(self, data: Optional[Buffer] = ...) -> MD4Hash: ...
+
+def new(data: Optional[Buffer] = ...) -> MD4Hash: ...
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD5.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD5.py
new file mode 100644
index 00000000..554b7772
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD5.py
@@ -0,0 +1,184 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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 *
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_md5_lib = load_pycryptodome_raw_lib("Crypto.Hash._MD5",
+ """
+ #define MD5_DIGEST_SIZE 16
+
+ int MD5_init(void **shaState);
+ int MD5_destroy(void *shaState);
+ int MD5_update(void *hs,
+ const uint8_t *buf,
+ size_t len);
+ int MD5_digest(const void *shaState,
+ uint8_t digest[MD5_DIGEST_SIZE]);
+ int MD5_copy(const void *src, void *dst);
+
+ int MD5_pbkdf2_hmac_assist(const void *inner,
+ const void *outer,
+ const uint8_t first_digest[MD5_DIGEST_SIZE],
+ uint8_t final_digest[MD5_DIGEST_SIZE],
+ size_t iterations);
+ """)
+
+class MD5Hash(object):
+ """A MD5 hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar block_size: the size in bytes of the internal message block,
+ input to the compression function
+ :vartype block_size: integer
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 16
+ # The internal block size of the hash algorithm in bytes.
+ block_size = 64
+ # ASN.1 Object ID
+ oid = "1.2.840.113549.2.5"
+
+ def __init__(self, data=None):
+ state = VoidPointer()
+ result = _raw_md5_lib.MD5_init(state.address_of())
+ if result:
+ raise ValueError("Error %d while instantiating MD5"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_md5_lib.MD5_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ result = _raw_md5_lib.MD5_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while instantiating MD5"
+ % result)
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_md5_lib.MD5_digest(self._state.get(),
+ bfr)
+ if result:
+ raise ValueError("Error %d while instantiating MD5"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = MD5Hash()
+ result = _raw_md5_lib.MD5_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying MD5" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh SHA-1 hash object."""
+
+ return MD5Hash(data)
+
+
+def new(data=None):
+ """Create a new hash object.
+
+ :parameter data:
+ Optional. The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`MD5Hash.update`.
+ :type data: byte string/byte array/memoryview
+
+ :Return: A :class:`MD5Hash` hash object
+ """
+ return MD5Hash().new(data)
+
+# The size of the resulting hash in bytes.
+digest_size = 16
+
+# The internal block size of the hash algorithm in bytes.
+block_size = 64
+
+
+def _pbkdf2_hmac_assist(inner, outer, first_digest, iterations):
+ """Compute the expensive inner loop in PBKDF-HMAC."""
+
+ assert len(first_digest) == digest_size
+ assert iterations > 0
+
+ bfr = create_string_buffer(digest_size);
+ result = _raw_md5_lib.MD5_pbkdf2_hmac_assist(
+ inner._state.get(),
+ outer._state.get(),
+ first_digest,
+ bfr,
+ c_size_t(iterations))
+
+ if result:
+ raise ValueError("Error %d with PBKDF2-HMAC assis for MD5" % result)
+
+ return get_raw_buffer(bfr)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD5.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD5.pyi
new file mode 100644
index 00000000..d8195562
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/MD5.pyi
@@ -0,0 +1,19 @@
+from typing import Union
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class MD5Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+
+ def __init__(self, data: Buffer = ...) -> None: ...
+ def update(self, data: Buffer) -> None: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> MD5Hash: ...
+ def new(self, data: Buffer = ...) -> MD5Hash: ...
+
+def new(data: Buffer = ...) -> MD5Hash: ...
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/Poly1305.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/Poly1305.py
new file mode 100644
index 00000000..eb5e0dad
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/Poly1305.py
@@ -0,0 +1,217 @@
+# -*- coding: utf-8 -*-
+#
+# Hash/Poly1305.py - Implements the Poly1305 MAC
+#
+# ===================================================================
+# 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 binascii import unhexlify
+
+from Crypto.Util.py3compat import bord, tobytes, _copy_bytes
+
+from Crypto.Hash import BLAKE2s
+from Crypto.Random import get_random_bytes
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+
+_raw_poly1305 = load_pycryptodome_raw_lib("Crypto.Hash._poly1305",
+ """
+ int poly1305_init(void **state,
+ const uint8_t *r,
+ size_t r_len,
+ const uint8_t *s,
+ size_t s_len);
+ int poly1305_destroy(void *state);
+ int poly1305_update(void *state,
+ const uint8_t *in,
+ size_t len);
+ int poly1305_digest(const void *state,
+ uint8_t *digest,
+ size_t len);
+ """)
+
+
+class Poly1305_MAC(object):
+ """An Poly1305 MAC object.
+ Do not instantiate directly. Use the :func:`new` function.
+
+ :ivar digest_size: the size in bytes of the resulting MAC tag
+ :vartype digest_size: integer
+ """
+
+ digest_size = 16
+
+ def __init__(self, r, s, data):
+
+ if len(r) != 16:
+ raise ValueError("Parameter r is not 16 bytes long")
+ if len(s) != 16:
+ raise ValueError("Parameter s is not 16 bytes long")
+
+ self._mac_tag = None
+
+ state = VoidPointer()
+ result = _raw_poly1305.poly1305_init(state.address_of(),
+ c_uint8_ptr(r),
+ c_size_t(len(r)),
+ c_uint8_ptr(s),
+ c_size_t(len(s))
+ )
+ if result:
+ raise ValueError("Error %d while instantiating Poly1305" % result)
+ self._state = SmartPointer(state.get(),
+ _raw_poly1305.poly1305_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Authenticate the next chunk of message.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of data
+ """
+
+ if self._mac_tag:
+ raise TypeError("You can only call 'digest' or 'hexdigest' on this object")
+
+ result = _raw_poly1305.poly1305_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while hashing Poly1305 data" % result)
+ return self
+
+ def copy(self):
+ raise NotImplementedError()
+
+ def digest(self):
+ """Return the **binary** (non-printable) MAC tag of the message
+ authenticated so far.
+
+ :return: The MAC tag digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ if self._mac_tag:
+ return self._mac_tag
+
+ bfr = create_string_buffer(16)
+ result = _raw_poly1305.poly1305_digest(self._state.get(),
+ bfr,
+ c_size_t(len(bfr)))
+ if result:
+ raise ValueError("Error %d while creating Poly1305 digest" % result)
+
+ self._mac_tag = get_raw_buffer(bfr)
+ return self._mac_tag
+
+ def hexdigest(self):
+ """Return the **printable** MAC tag of the message authenticated so far.
+
+ :return: The MAC tag, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x)
+ for x in tuple(self.digest())])
+
+ def verify(self, mac_tag):
+ """Verify that a given **binary** MAC (computed by another party)
+ is valid.
+
+ Args:
+ mac_tag (byte string/byte string/memoryview): the expected MAC of the message.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ secret = get_random_bytes(16)
+
+ mac1 = BLAKE2s.new(digest_bits=160, key=secret, data=mac_tag)
+ mac2 = BLAKE2s.new(digest_bits=160, key=secret, data=self.digest())
+
+ if mac1.digest() != mac2.digest():
+ raise ValueError("MAC check failed")
+
+ def hexverify(self, hex_mac_tag):
+ """Verify that a given **printable** MAC (computed by another party)
+ is valid.
+
+ Args:
+ hex_mac_tag (string): the expected MAC of the message,
+ as a hexadecimal string.
+
+ Raises:
+ ValueError: if the MAC does not match. It means that the message
+ has been tampered with or that the MAC key is incorrect.
+ """
+
+ self.verify(unhexlify(tobytes(hex_mac_tag)))
+
+
+
+def new(**kwargs):
+ """Create a new Poly1305 MAC object.
+
+ Args:
+ key (bytes/bytearray/memoryview):
+ The 32-byte key for the Poly1305 object.
+ cipher (module from ``Crypto.Cipher``):
+ The cipher algorithm to use for deriving the Poly1305
+ key pair *(r, s)*.
+ It can only be ``Crypto.Cipher.AES`` or ``Crypto.Cipher.ChaCha20``.
+ nonce (bytes/bytearray/memoryview):
+ Optional. The non-repeatable value to use for the MAC of this message.
+ It must be 16 bytes long for ``AES`` and 8 or 12 bytes for ``ChaCha20``.
+ If not passed, a random nonce is created; you will find it in the
+ ``nonce`` attribute of the new object.
+ data (bytes/bytearray/memoryview):
+ Optional. The very first chunk of the message to authenticate.
+ It is equivalent to an early call to ``update()``.
+
+ Returns:
+ A :class:`Poly1305_MAC` object
+ """
+
+ cipher = kwargs.pop("cipher", None)
+ if not hasattr(cipher, '_derive_Poly1305_key_pair'):
+ raise ValueError("Parameter 'cipher' must be AES or ChaCha20")
+
+ cipher_key = kwargs.pop("key", None)
+ if cipher_key is None:
+ raise TypeError("You must pass a parameter 'key'")
+
+ nonce = kwargs.pop("nonce", None)
+ data = kwargs.pop("data", None)
+
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ r, s, nonce = cipher._derive_Poly1305_key_pair(cipher_key, nonce)
+
+ new_mac = Poly1305_MAC(r, s, data)
+ new_mac.nonce = _copy_bytes(None, None, nonce) # nonce may still be just a memoryview
+ return new_mac
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/Poly1305.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/Poly1305.pyi
new file mode 100644
index 00000000..f97a14a0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/Poly1305.pyi
@@ -0,0 +1,24 @@
+from types import ModuleType
+from typing import Union
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class Poly1305_MAC(object):
+ block_size: int
+ digest_size: int
+ oid: str
+
+ def __init__(self,
+ r : int,
+ s : int,
+ data : Buffer) -> None: ...
+ def update(self, data: Buffer) -> Poly1305_MAC: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def verify(self, mac_tag: Buffer) -> None: ...
+ def hexverify(self, hex_mac_tag: str) -> None: ...
+
+def new(key: Buffer,
+ cipher: ModuleType,
+ nonce: Buffer = ...,
+ data: Buffer = ...) -> Poly1305_MAC: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD.py
new file mode 100644
index 00000000..4e802358
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+# This file exists for backward compatibility with old code that refers to
+# Crypto.Hash.RIPEMD
+
+"""Deprecated alias for `Crypto.Hash.RIPEMD160`"""
+
+from Crypto.Hash.RIPEMD160 import new, block_size, digest_size
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD.pyi
new file mode 100644
index 00000000..e33eb2df
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD.pyi
@@ -0,0 +1,3 @@
+# This file exists for backward compatibility with old code that refers to
+# Crypto.Hash.SHA
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD160.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD160.py
new file mode 100644
index 00000000..820b57dd
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD160.py
@@ -0,0 +1,169 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.Util.py3compat import bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_ripemd160_lib = load_pycryptodome_raw_lib(
+ "Crypto.Hash._RIPEMD160",
+ """
+ int ripemd160_init(void **shaState);
+ int ripemd160_destroy(void *shaState);
+ int ripemd160_update(void *hs,
+ const uint8_t *buf,
+ size_t len);
+ int ripemd160_digest(const void *shaState,
+ uint8_t digest[20]);
+ int ripemd160_copy(const void *src, void *dst);
+ """)
+
+
+class RIPEMD160Hash(object):
+ """A RIPEMD-160 hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar block_size: the size in bytes of the internal message block,
+ input to the compression function
+ :vartype block_size: integer
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 20
+ # The internal block size of the hash algorithm in bytes.
+ block_size = 64
+ # ASN.1 Object ID
+ oid = "1.3.36.3.2.1"
+
+ def __init__(self, data=None):
+ state = VoidPointer()
+ result = _raw_ripemd160_lib.ripemd160_init(state.address_of())
+ if result:
+ raise ValueError("Error %d while instantiating RIPEMD160"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_ripemd160_lib.ripemd160_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ result = _raw_ripemd160_lib.ripemd160_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while instantiating ripemd160"
+ % result)
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_ripemd160_lib.ripemd160_digest(self._state.get(),
+ bfr)
+ if result:
+ raise ValueError("Error %d while instantiating ripemd160"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = RIPEMD160Hash()
+ result = _raw_ripemd160_lib.ripemd160_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying ripemd160" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh RIPEMD-160 hash object."""
+
+ return RIPEMD160Hash(data)
+
+
+def new(data=None):
+ """Create a new hash object.
+
+ :parameter data:
+ Optional. The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`RIPEMD160Hash.update`.
+ :type data: byte string/byte array/memoryview
+
+ :Return: A :class:`RIPEMD160Hash` hash object
+ """
+
+ return RIPEMD160Hash().new(data)
+
+# The size of the resulting hash in bytes.
+digest_size = RIPEMD160Hash.digest_size
+
+# The internal block size of the hash algorithm in bytes.
+block_size = RIPEMD160Hash.block_size
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD160.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD160.pyi
new file mode 100644
index 00000000..b6194737
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/RIPEMD160.pyi
@@ -0,0 +1,19 @@
+from typing import Union
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class RIPEMD160Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+
+ def __init__(self, data: Buffer = ...) -> None: ...
+ def update(self, data: Buffer) -> None: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> RIPEMD160Hash: ...
+ def new(self, data: Buffer = ...) -> RIPEMD160Hash: ...
+
+def new(data: Buffer = ...) -> RIPEMD160Hash: ...
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA.py
new file mode 100644
index 00000000..0cc141c9
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+# This file exists for backward compatibility with old code that refers to
+# Crypto.Hash.SHA
+
+from Crypto.Hash.SHA1 import __doc__, new, block_size, digest_size
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA.pyi
new file mode 100644
index 00000000..4d7d57e2
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA.pyi
@@ -0,0 +1,4 @@
+# This file exists for backward compatibility with old code that refers to
+# Crypto.Hash.SHA
+
+from Crypto.Hash.SHA1 import __doc__, new, block_size, digest_size
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA1.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA1.py
new file mode 100644
index 00000000..f79d8257
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA1.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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 *
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_sha1_lib = load_pycryptodome_raw_lib("Crypto.Hash._SHA1",
+ """
+ #define SHA1_DIGEST_SIZE 20
+
+ int SHA1_init(void **shaState);
+ int SHA1_destroy(void *shaState);
+ int SHA1_update(void *hs,
+ const uint8_t *buf,
+ size_t len);
+ int SHA1_digest(const void *shaState,
+ uint8_t digest[SHA1_DIGEST_SIZE]);
+ int SHA1_copy(const void *src, void *dst);
+
+ int SHA1_pbkdf2_hmac_assist(const void *inner,
+ const void *outer,
+ const uint8_t first_digest[SHA1_DIGEST_SIZE],
+ uint8_t final_digest[SHA1_DIGEST_SIZE],
+ size_t iterations);
+ """)
+
+class SHA1Hash(object):
+ """A SHA-1 hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar block_size: the size in bytes of the internal message block,
+ input to the compression function
+ :vartype block_size: integer
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 20
+ # The internal block size of the hash algorithm in bytes.
+ block_size = 64
+ # ASN.1 Object ID
+ oid = "1.3.14.3.2.26"
+
+ def __init__(self, data=None):
+ state = VoidPointer()
+ result = _raw_sha1_lib.SHA1_init(state.address_of())
+ if result:
+ raise ValueError("Error %d while instantiating SHA1"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_sha1_lib.SHA1_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ result = _raw_sha1_lib.SHA1_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while instantiating SHA1"
+ % result)
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_sha1_lib.SHA1_digest(self._state.get(),
+ bfr)
+ if result:
+ raise ValueError("Error %d while instantiating SHA1"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = SHA1Hash()
+ result = _raw_sha1_lib.SHA1_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHA1" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh SHA-1 hash object."""
+
+ return SHA1Hash(data)
+
+
+def new(data=None):
+ """Create a new hash object.
+
+ :parameter data:
+ Optional. The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`SHA1Hash.update`.
+ :type data: byte string/byte array/memoryview
+
+ :Return: A :class:`SHA1Hash` hash object
+ """
+ return SHA1Hash().new(data)
+
+
+# The size of the resulting hash in bytes.
+digest_size = SHA1Hash.digest_size
+
+# The internal block size of the hash algorithm in bytes.
+block_size = SHA1Hash.block_size
+
+
+def _pbkdf2_hmac_assist(inner, outer, first_digest, iterations):
+ """Compute the expensive inner loop in PBKDF-HMAC."""
+
+ assert len(first_digest) == digest_size
+ assert iterations > 0
+
+ bfr = create_string_buffer(digest_size);
+ result = _raw_sha1_lib.SHA1_pbkdf2_hmac_assist(
+ inner._state.get(),
+ outer._state.get(),
+ first_digest,
+ bfr,
+ c_size_t(iterations))
+
+ if result:
+ raise ValueError("Error %d with PBKDF2-HMAC assis for SHA1" % result)
+
+ return get_raw_buffer(bfr)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA1.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA1.pyi
new file mode 100644
index 00000000..d6c8e25e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA1.pyi
@@ -0,0 +1,19 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class SHA1Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+
+ def __init__(self, data: Optional[Buffer] = ...) -> None: ...
+ def update(self, data: Buffer) -> None: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> SHA1Hash: ...
+ def new(self, data: Optional[Buffer] = ...) -> SHA1Hash: ...
+
+def new(data: Optional[Buffer] = ...) -> SHA1Hash: ...
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA224.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA224.py
new file mode 100644
index 00000000..f788b063
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA224.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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 bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_sha224_lib = load_pycryptodome_raw_lib("Crypto.Hash._SHA224",
+ """
+ int SHA224_init(void **shaState);
+ int SHA224_destroy(void *shaState);
+ int SHA224_update(void *hs,
+ const uint8_t *buf,
+ size_t len);
+ int SHA224_digest(const void *shaState,
+ uint8_t *digest,
+ size_t digest_size);
+ int SHA224_copy(const void *src, void *dst);
+
+ int SHA224_pbkdf2_hmac_assist(const void *inner,
+ const void *outer,
+ const uint8_t *first_digest,
+ uint8_t *final_digest,
+ size_t iterations,
+ size_t digest_size);
+ """)
+
+class SHA224Hash(object):
+ """A SHA-224 hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar block_size: the size in bytes of the internal message block,
+ input to the compression function
+ :vartype block_size: integer
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 28
+ # The internal block size of the hash algorithm in bytes.
+ block_size = 64
+ # ASN.1 Object ID
+ oid = '2.16.840.1.101.3.4.2.4'
+
+ def __init__(self, data=None):
+ state = VoidPointer()
+ result = _raw_sha224_lib.SHA224_init(state.address_of())
+ if result:
+ raise ValueError("Error %d while instantiating SHA224"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_sha224_lib.SHA224_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ result = _raw_sha224_lib.SHA224_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while hashing data with SHA224"
+ % result)
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_sha224_lib.SHA224_digest(self._state.get(),
+ bfr,
+ c_size_t(self.digest_size))
+ if result:
+ raise ValueError("Error %d while making SHA224 digest"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = SHA224Hash()
+ result = _raw_sha224_lib.SHA224_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHA224" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh SHA-224 hash object."""
+
+ return SHA224Hash(data)
+
+
+def new(data=None):
+ """Create a new hash object.
+
+ :parameter data:
+ Optional. The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`SHA224Hash.update`.
+ :type data: byte string/byte array/memoryview
+
+ :Return: A :class:`SHA224Hash` hash object
+ """
+ return SHA224Hash().new(data)
+
+
+# The size of the resulting hash in bytes.
+digest_size = SHA224Hash.digest_size
+
+# The internal block size of the hash algorithm in bytes.
+block_size = SHA224Hash.block_size
+
+
+def _pbkdf2_hmac_assist(inner, outer, first_digest, iterations):
+ """Compute the expensive inner loop in PBKDF-HMAC."""
+
+ assert iterations > 0
+
+ bfr = create_string_buffer(len(first_digest));
+ result = _raw_sha224_lib.SHA224_pbkdf2_hmac_assist(
+ inner._state.get(),
+ outer._state.get(),
+ first_digest,
+ bfr,
+ c_size_t(iterations),
+ c_size_t(len(first_digest)))
+
+ if result:
+ raise ValueError("Error %d with PBKDF2-HMAC assist for SHA224" % result)
+
+ return get_raw_buffer(bfr)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA224.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA224.pyi
new file mode 100644
index 00000000..613a7f9a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA224.pyi
@@ -0,0 +1,19 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class SHA224Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+
+ def __init__(self, data: Optional[Buffer] = ...) -> None: ...
+ def update(self, data: Buffer) -> None: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> SHA224Hash: ...
+ def new(self, data: Optional[Buffer] = ...) -> SHA224Hash: ...
+
+def new(data: Optional[Buffer] = ...) -> SHA224Hash: ...
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA256.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA256.py
new file mode 100644
index 00000000..957aa37e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA256.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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 bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_sha256_lib = load_pycryptodome_raw_lib("Crypto.Hash._SHA256",
+ """
+ int SHA256_init(void **shaState);
+ int SHA256_destroy(void *shaState);
+ int SHA256_update(void *hs,
+ const uint8_t *buf,
+ size_t len);
+ int SHA256_digest(const void *shaState,
+ uint8_t *digest,
+ size_t digest_size);
+ int SHA256_copy(const void *src, void *dst);
+
+ int SHA256_pbkdf2_hmac_assist(const void *inner,
+ const void *outer,
+ const uint8_t *first_digest,
+ uint8_t *final_digest,
+ size_t iterations,
+ size_t digest_size);
+ """)
+
+class SHA256Hash(object):
+ """A SHA-256 hash object.
+ Do not instantiate directly. Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar block_size: the size in bytes of the internal message block,
+ input to the compression function
+ :vartype block_size: integer
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 32
+ # The internal block size of the hash algorithm in bytes.
+ block_size = 64
+ # ASN.1 Object ID
+ oid = "2.16.840.1.101.3.4.2.1"
+
+ def __init__(self, data=None):
+ state = VoidPointer()
+ result = _raw_sha256_lib.SHA256_init(state.address_of())
+ if result:
+ raise ValueError("Error %d while instantiating SHA256"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_sha256_lib.SHA256_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ result = _raw_sha256_lib.SHA256_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while hashing data with SHA256"
+ % result)
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_sha256_lib.SHA256_digest(self._state.get(),
+ bfr,
+ c_size_t(self.digest_size))
+ if result:
+ raise ValueError("Error %d while making SHA256 digest"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = SHA256Hash()
+ result = _raw_sha256_lib.SHA256_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHA256" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh SHA-256 hash object."""
+
+ return SHA256Hash(data)
+
+def new(data=None):
+ """Create a new hash object.
+
+ :parameter data:
+ Optional. The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`SHA256Hash.update`.
+ :type data: byte string/byte array/memoryview
+
+ :Return: A :class:`SHA256Hash` hash object
+ """
+
+ return SHA256Hash().new(data)
+
+
+# The size of the resulting hash in bytes.
+digest_size = SHA256Hash.digest_size
+
+# The internal block size of the hash algorithm in bytes.
+block_size = SHA256Hash.block_size
+
+
+def _pbkdf2_hmac_assist(inner, outer, first_digest, iterations):
+ """Compute the expensive inner loop in PBKDF-HMAC."""
+
+ assert iterations > 0
+
+ bfr = create_string_buffer(len(first_digest));
+ result = _raw_sha256_lib.SHA256_pbkdf2_hmac_assist(
+ inner._state.get(),
+ outer._state.get(),
+ first_digest,
+ bfr,
+ c_size_t(iterations),
+ c_size_t(len(first_digest)))
+
+ if result:
+ raise ValueError("Error %d with PBKDF2-HMAC assist for SHA256" % result)
+
+ return get_raw_buffer(bfr)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA256.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA256.pyi
new file mode 100644
index 00000000..cbf21bfb
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA256.pyi
@@ -0,0 +1,18 @@
+from typing import Union, Optional
+
+
+class SHA256Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+ def __init__(self, data: Optional[Union[bytes, bytearray, memoryview]]=None) -> None: ...
+ def update(self, data: Union[bytes, bytearray, memoryview]) -> None: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> SHA256Hash: ...
+ def new(self, data: Optional[Union[bytes, bytearray, memoryview]]=None) -> SHA256Hash: ...
+
+def new(data: Optional[Union[bytes, bytearray, memoryview]]=None) -> SHA256Hash: ...
+
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA384.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA384.py
new file mode 100644
index 00000000..a98fa9a3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA384.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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 bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_sha384_lib = load_pycryptodome_raw_lib("Crypto.Hash._SHA384",
+ """
+ int SHA384_init(void **shaState);
+ int SHA384_destroy(void *shaState);
+ int SHA384_update(void *hs,
+ const uint8_t *buf,
+ size_t len);
+ int SHA384_digest(const void *shaState,
+ uint8_t *digest,
+ size_t digest_size);
+ int SHA384_copy(const void *src, void *dst);
+
+ int SHA384_pbkdf2_hmac_assist(const void *inner,
+ const void *outer,
+ const uint8_t *first_digest,
+ uint8_t *final_digest,
+ size_t iterations,
+ size_t digest_size);
+ """)
+
+class SHA384Hash(object):
+ """A SHA-384 hash object.
+ Do not instantiate directly. Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar block_size: the size in bytes of the internal message block,
+ input to the compression function
+ :vartype block_size: integer
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 48
+ # The internal block size of the hash algorithm in bytes.
+ block_size = 128
+ # ASN.1 Object ID
+ oid = '2.16.840.1.101.3.4.2.2'
+
+ def __init__(self, data=None):
+ state = VoidPointer()
+ result = _raw_sha384_lib.SHA384_init(state.address_of())
+ if result:
+ raise ValueError("Error %d while instantiating SHA384"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_sha384_lib.SHA384_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ result = _raw_sha384_lib.SHA384_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while hashing data with SHA384"
+ % result)
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_sha384_lib.SHA384_digest(self._state.get(),
+ bfr,
+ c_size_t(self.digest_size))
+ if result:
+ raise ValueError("Error %d while making SHA384 digest"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = SHA384Hash()
+ result = _raw_sha384_lib.SHA384_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHA384" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh SHA-384 hash object."""
+
+ return SHA384Hash(data)
+
+
+def new(data=None):
+ """Create a new hash object.
+
+ :parameter data:
+ Optional. The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`SHA384Hash.update`.
+ :type data: byte string/byte array/memoryview
+
+ :Return: A :class:`SHA384Hash` hash object
+ """
+
+ return SHA384Hash().new(data)
+
+
+# The size of the resulting hash in bytes.
+digest_size = SHA384Hash.digest_size
+
+# The internal block size of the hash algorithm in bytes.
+block_size = SHA384Hash.block_size
+
+
+def _pbkdf2_hmac_assist(inner, outer, first_digest, iterations):
+ """Compute the expensive inner loop in PBKDF-HMAC."""
+
+ assert iterations > 0
+
+ bfr = create_string_buffer(len(first_digest));
+ result = _raw_sha384_lib.SHA384_pbkdf2_hmac_assist(
+ inner._state.get(),
+ outer._state.get(),
+ first_digest,
+ bfr,
+ c_size_t(iterations),
+ c_size_t(len(first_digest)))
+
+ if result:
+ raise ValueError("Error %d with PBKDF2-HMAC assist for SHA384" % result)
+
+ return get_raw_buffer(bfr)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA384.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA384.pyi
new file mode 100644
index 00000000..c2aab9e0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA384.pyi
@@ -0,0 +1,19 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class SHA384Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+
+ def __init__(self, data: Optional[Buffer] = ...) -> None: ...
+ def update(self, data: Buffer) -> None: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> SHA384Hash: ...
+ def new(self, data: Optional[Buffer] = ...) -> SHA384Hash: ...
+
+def new(data: Optional[Buffer] = ...) -> SHA384Hash: ...
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_224.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_224.py
new file mode 100644
index 00000000..54556d00
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_224.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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 bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr, c_ubyte)
+
+from Crypto.Hash.keccak import _raw_keccak_lib
+
+class SHA3_224_Hash(object):
+ """A SHA3-224 hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 28
+
+ # ASN.1 Object ID
+ oid = "2.16.840.1.101.3.4.2.7"
+
+ # Input block size for HMAC
+ block_size = 144
+
+ def __init__(self, data, update_after_digest):
+ self._update_after_digest = update_after_digest
+ self._digest_done = False
+ self._padding = 0x06
+
+ state = VoidPointer()
+ result = _raw_keccak_lib.keccak_init(state.address_of(),
+ c_size_t(self.digest_size * 2),
+ c_ubyte(24))
+ if result:
+ raise ValueError("Error %d while instantiating SHA-3/224"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_keccak_lib.keccak_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._digest_done and not self._update_after_digest:
+ raise TypeError("You can only call 'digest' or 'hexdigest' on this object")
+
+ result = _raw_keccak_lib.keccak_absorb(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data))
+ )
+ if result:
+ raise ValueError("Error %d while updating SHA-3/224"
+ % result)
+ return self
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ self._digest_done = True
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_keccak_lib.keccak_digest(self._state.get(),
+ bfr,
+ c_size_t(self.digest_size),
+ c_ubyte(self._padding))
+ if result:
+ raise ValueError("Error %d while instantiating SHA-3/224"
+ % result)
+
+ self._digest_value = get_raw_buffer(bfr)
+ return self._digest_value
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = self.new()
+ result = _raw_keccak_lib.keccak_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHA3-224" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh SHA3-224 hash object."""
+
+ return type(self)(data, self._update_after_digest)
+
+
+def new(*args, **kwargs):
+ """Create a new hash object.
+
+ Args:
+ data (byte string/byte array/memoryview):
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+ update_after_digest (boolean):
+ Whether :meth:`digest` can be followed by another :meth:`update`
+ (default: ``False``).
+
+ :Return: A :class:`SHA3_224_Hash` hash object
+ """
+
+ data = kwargs.pop("data", None)
+ update_after_digest = kwargs.pop("update_after_digest", False)
+ if len(args) == 1:
+ if data:
+ raise ValueError("Initial data for hash specified twice")
+ data = args[0]
+
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ return SHA3_224_Hash(data, update_after_digest)
+
+# The size of the resulting hash in bytes.
+digest_size = SHA3_224_Hash.digest_size
+
+# Input block size for HMAC
+block_size = 144
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_224.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_224.pyi
new file mode 100644
index 00000000..21808219
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_224.pyi
@@ -0,0 +1,19 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class SHA3_224_Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+ def __init__(self, data: Optional[Buffer], update_after_digest: bool) -> None: ...
+ def update(self, data: Buffer) -> SHA3_224_Hash: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> SHA3_224_Hash: ...
+ def new(self, data: Optional[Buffer]) -> SHA3_224_Hash: ...
+
+def new(__data: Buffer = ..., update_after_digest: bool = ...) -> SHA3_224_Hash: ...
+
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_256.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_256.py
new file mode 100644
index 00000000..b4f11ee1
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_256.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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 bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr, c_ubyte)
+
+from Crypto.Hash.keccak import _raw_keccak_lib
+
+class SHA3_256_Hash(object):
+ """A SHA3-256 hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 32
+
+ # ASN.1 Object ID
+ oid = "2.16.840.1.101.3.4.2.8"
+
+ # Input block size for HMAC
+ block_size = 136
+
+ def __init__(self, data, update_after_digest):
+ self._update_after_digest = update_after_digest
+ self._digest_done = False
+ self._padding = 0x06
+
+ state = VoidPointer()
+ result = _raw_keccak_lib.keccak_init(state.address_of(),
+ c_size_t(self.digest_size * 2),
+ c_ubyte(24))
+ if result:
+ raise ValueError("Error %d while instantiating SHA-3/256"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_keccak_lib.keccak_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._digest_done and not self._update_after_digest:
+ raise TypeError("You can only call 'digest' or 'hexdigest' on this object")
+
+ result = _raw_keccak_lib.keccak_absorb(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data))
+ )
+ if result:
+ raise ValueError("Error %d while updating SHA-3/256"
+ % result)
+ return self
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ self._digest_done = True
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_keccak_lib.keccak_digest(self._state.get(),
+ bfr,
+ c_size_t(self.digest_size),
+ c_ubyte(self._padding))
+ if result:
+ raise ValueError("Error %d while instantiating SHA-3/256"
+ % result)
+
+ self._digest_value = get_raw_buffer(bfr)
+ return self._digest_value
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = self.new()
+ result = _raw_keccak_lib.keccak_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHA3-256" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh SHA3-256 hash object."""
+
+ return type(self)(data, self._update_after_digest)
+
+
+def new(*args, **kwargs):
+ """Create a new hash object.
+
+ Args:
+ data (byte string/byte array/memoryview):
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+ update_after_digest (boolean):
+ Whether :meth:`digest` can be followed by another :meth:`update`
+ (default: ``False``).
+
+ :Return: A :class:`SHA3_256_Hash` hash object
+ """
+
+ data = kwargs.pop("data", None)
+ update_after_digest = kwargs.pop("update_after_digest", False)
+ if len(args) == 1:
+ if data:
+ raise ValueError("Initial data for hash specified twice")
+ data = args[0]
+
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ return SHA3_256_Hash(data, update_after_digest)
+
+# The size of the resulting hash in bytes.
+digest_size = SHA3_256_Hash.digest_size
+
+# Input block size for HMAC
+block_size = 136
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_256.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_256.pyi
new file mode 100644
index 00000000..88436bd8
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_256.pyi
@@ -0,0 +1,19 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class SHA3_256_Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+ def __init__(self, data: Optional[Buffer], update_after_digest: bool) -> None: ...
+ def update(self, data: Buffer) -> SHA3_256_Hash: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> SHA3_256_Hash: ...
+ def new(self, data: Optional[Buffer]) -> SHA3_256_Hash: ...
+
+def new(__data: Buffer = ..., update_after_digest: bool = ...) -> SHA3_256_Hash: ...
+
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_384.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_384.py
new file mode 100644
index 00000000..12f61ce5
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_384.py
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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 bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr, c_ubyte)
+
+from Crypto.Hash.keccak import _raw_keccak_lib
+
+class SHA3_384_Hash(object):
+ """A SHA3-384 hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 48
+
+ # ASN.1 Object ID
+ oid = "2.16.840.1.101.3.4.2.9"
+
+ # Input block size for HMAC
+ block_size = 104
+
+ def __init__(self, data, update_after_digest):
+ self._update_after_digest = update_after_digest
+ self._digest_done = False
+ self._padding = 0x06
+
+ state = VoidPointer()
+ result = _raw_keccak_lib.keccak_init(state.address_of(),
+ c_size_t(self.digest_size * 2),
+ c_ubyte(24))
+ if result:
+ raise ValueError("Error %d while instantiating SHA-3/384"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_keccak_lib.keccak_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._digest_done and not self._update_after_digest:
+ raise TypeError("You can only call 'digest' or 'hexdigest' on this object")
+
+ result = _raw_keccak_lib.keccak_absorb(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while updating SHA-3/384"
+ % result)
+ return self
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ self._digest_done = True
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_keccak_lib.keccak_digest(self._state.get(),
+ bfr,
+ c_size_t(self.digest_size),
+ c_ubyte(self._padding))
+ if result:
+ raise ValueError("Error %d while instantiating SHA-3/384"
+ % result)
+
+ self._digest_value = get_raw_buffer(bfr)
+ return self._digest_value
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = self.new()
+ result = _raw_keccak_lib.keccak_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHA3-384" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh SHA3-256 hash object."""
+
+ return type(self)(data, self._update_after_digest)
+
+
+ def new(self, data=None):
+ """Create a fresh SHA3-384 hash object."""
+
+ return type(self)(data, self._update_after_digest)
+
+
+def new(*args, **kwargs):
+ """Create a new hash object.
+
+ Args:
+ data (byte string/byte array/memoryview):
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+ update_after_digest (boolean):
+ Whether :meth:`digest` can be followed by another :meth:`update`
+ (default: ``False``).
+
+ :Return: A :class:`SHA3_384_Hash` hash object
+ """
+
+ data = kwargs.pop("data", None)
+ update_after_digest = kwargs.pop("update_after_digest", False)
+ if len(args) == 1:
+ if data:
+ raise ValueError("Initial data for hash specified twice")
+ data = args[0]
+
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ return SHA3_384_Hash(data, update_after_digest)
+
+# The size of the resulting hash in bytes.
+digest_size = SHA3_384_Hash.digest_size
+
+# Input block size for HMAC
+block_size = 104
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_384.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_384.pyi
new file mode 100644
index 00000000..98d00c6c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_384.pyi
@@ -0,0 +1,19 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class SHA3_384_Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+ def __init__(self, data: Optional[Buffer], update_after_digest: bool) -> None: ...
+ def update(self, data: Buffer) -> SHA3_384_Hash: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> SHA3_384_Hash: ...
+ def new(self, data: Optional[Buffer]) -> SHA3_384_Hash: ...
+
+def new(__data: Buffer = ..., update_after_digest: bool = ...) -> SHA3_384_Hash: ...
+
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_512.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_512.py
new file mode 100644
index 00000000..de8880c7
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_512.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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 bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr, c_ubyte)
+
+from Crypto.Hash.keccak import _raw_keccak_lib
+
+class SHA3_512_Hash(object):
+ """A SHA3-512 hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The size of the resulting hash in bytes.
+ digest_size = 64
+
+ # ASN.1 Object ID
+ oid = "2.16.840.1.101.3.4.2.10"
+
+ # Input block size for HMAC
+ block_size = 72
+
+ def __init__(self, data, update_after_digest):
+ self._update_after_digest = update_after_digest
+ self._digest_done = False
+ self._padding = 0x06
+
+ state = VoidPointer()
+ result = _raw_keccak_lib.keccak_init(state.address_of(),
+ c_size_t(self.digest_size * 2),
+ c_ubyte(24))
+ if result:
+ raise ValueError("Error %d while instantiating SHA-3/512"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_keccak_lib.keccak_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._digest_done and not self._update_after_digest:
+ raise TypeError("You can only call 'digest' or 'hexdigest' on this object")
+
+ result = _raw_keccak_lib.keccak_absorb(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while updating SHA-3/512"
+ % result)
+ return self
+
+ def digest(self):
+
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ self._digest_done = True
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_keccak_lib.keccak_digest(self._state.get(),
+ bfr,
+ c_size_t(self.digest_size),
+ c_ubyte(self._padding))
+ if result:
+ raise ValueError("Error %d while instantiating SHA-3/512"
+ % result)
+
+ self._digest_value = get_raw_buffer(bfr)
+ return self._digest_value
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = self.new()
+ result = _raw_keccak_lib.keccak_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHA3-512" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh SHA3-521 hash object."""
+
+ return type(self)(data, self._update_after_digest)
+
+
+def new(*args, **kwargs):
+ """Create a new hash object.
+
+ Args:
+ data (byte string/byte array/memoryview):
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+ update_after_digest (boolean):
+ Whether :meth:`digest` can be followed by another :meth:`update`
+ (default: ``False``).
+
+ :Return: A :class:`SHA3_512_Hash` hash object
+ """
+
+ data = kwargs.pop("data", None)
+ update_after_digest = kwargs.pop("update_after_digest", False)
+ if len(args) == 1:
+ if data:
+ raise ValueError("Initial data for hash specified twice")
+ data = args[0]
+
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ return SHA3_512_Hash(data, update_after_digest)
+
+# The size of the resulting hash in bytes.
+digest_size = SHA3_512_Hash.digest_size
+
+# Input block size for HMAC
+block_size = 72
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_512.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_512.pyi
new file mode 100644
index 00000000..cdeec165
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA3_512.pyi
@@ -0,0 +1,19 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class SHA3_512_Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+ def __init__(self, data: Optional[Buffer], update_after_digest: bool) -> None: ...
+ def update(self, data: Buffer) -> SHA3_512_Hash: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> SHA3_512_Hash: ...
+ def new(self, data: Optional[Buffer]) -> SHA3_512_Hash: ...
+
+def new(__data: Buffer = ..., update_after_digest: bool = ...) -> SHA3_512_Hash: ...
+
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA512.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA512.py
new file mode 100644
index 00000000..403fe45e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA512.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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 bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr)
+
+_raw_sha512_lib = load_pycryptodome_raw_lib("Crypto.Hash._SHA512",
+ """
+ int SHA512_init(void **shaState,
+ size_t digest_size);
+ int SHA512_destroy(void *shaState);
+ int SHA512_update(void *hs,
+ const uint8_t *buf,
+ size_t len);
+ int SHA512_digest(const void *shaState,
+ uint8_t *digest,
+ size_t digest_size);
+ int SHA512_copy(const void *src, void *dst);
+
+ int SHA512_pbkdf2_hmac_assist(const void *inner,
+ const void *outer,
+ const uint8_t *first_digest,
+ uint8_t *final_digest,
+ size_t iterations,
+ size_t digest_size);
+ """)
+
+class SHA512Hash(object):
+ """A SHA-512 hash object (possibly in its truncated version SHA-512/224 or
+ SHA-512/256.
+ Do not instantiate directly. Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+
+ :ivar block_size: the size in bytes of the internal message block,
+ input to the compression function
+ :vartype block_size: integer
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ # The internal block size of the hash algorithm in bytes.
+ block_size = 128
+
+ def __init__(self, data, truncate):
+ self._truncate = truncate
+
+ if truncate is None:
+ self.oid = "2.16.840.1.101.3.4.2.3"
+ self.digest_size = 64
+ elif truncate == "224":
+ self.oid = "2.16.840.1.101.3.4.2.5"
+ self.digest_size = 28
+ elif truncate == "256":
+ self.oid = "2.16.840.1.101.3.4.2.6"
+ self.digest_size = 32
+ else:
+ raise ValueError("Incorrect truncation length. It must be '224' or '256'.")
+
+ state = VoidPointer()
+ result = _raw_sha512_lib.SHA512_init(state.address_of(),
+ c_size_t(self.digest_size))
+ if result:
+ raise ValueError("Error %d while instantiating SHA-512"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_sha512_lib.SHA512_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ result = _raw_sha512_lib.SHA512_update(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while hashing data with SHA512"
+ % result)
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_sha512_lib.SHA512_digest(self._state.get(),
+ bfr,
+ c_size_t(self.digest_size))
+ if result:
+ raise ValueError("Error %d while making SHA512 digest"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+ This can be used to efficiently compute the digests of strings that
+ share a common initial substring.
+
+ :return: A hash object of the same type
+ """
+
+ clone = SHA512Hash(None, self._truncate)
+ result = _raw_sha512_lib.SHA512_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHA512" % result)
+ return clone
+
+ def new(self, data=None):
+ """Create a fresh SHA-512 hash object."""
+
+ return SHA512Hash(data, self._truncate)
+
+
+def new(data=None, truncate=None):
+ """Create a new hash object.
+
+ Args:
+ data (bytes/bytearray/memoryview):
+ Optional. The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`SHA512Hash.update`.
+ truncate (string):
+ Optional. The desired length of the digest. It can be either "224" or
+ "256". If not present, the digest is 512 bits long.
+ Passing this parameter is **not** equivalent to simply truncating
+ the output digest.
+
+ :Return: A :class:`SHA512Hash` hash object
+ """
+
+ return SHA512Hash(data, truncate)
+
+
+# The size of the full SHA-512 hash in bytes.
+digest_size = 64
+
+# The internal block size of the hash algorithm in bytes.
+block_size = 128
+
+
+def _pbkdf2_hmac_assist(inner, outer, first_digest, iterations):
+ """Compute the expensive inner loop in PBKDF-HMAC."""
+
+ assert iterations > 0
+
+ bfr = create_string_buffer(len(first_digest));
+ result = _raw_sha512_lib.SHA512_pbkdf2_hmac_assist(
+ inner._state.get(),
+ outer._state.get(),
+ first_digest,
+ bfr,
+ c_size_t(iterations),
+ c_size_t(len(first_digest)))
+
+ if result:
+ raise ValueError("Error %d with PBKDF2-HMAC assist for SHA512" % result)
+
+ return get_raw_buffer(bfr)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA512.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA512.pyi
new file mode 100644
index 00000000..f219ee94
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHA512.pyi
@@ -0,0 +1,22 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class SHA512Hash(object):
+ digest_size: int
+ block_size: int
+ oid: str
+
+ def __init__(self,
+ data: Optional[Buffer],
+ truncate: Optional[str]) -> None: ...
+ def update(self, data: Buffer) -> None: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def copy(self) -> SHA512Hash: ...
+ def new(self, data: Optional[Buffer] = ...) -> SHA512Hash: ...
+
+def new(data: Optional[Buffer] = ...,
+ truncate: Optional[str] = ...) -> SHA512Hash: ...
+digest_size: int
+block_size: int
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE128.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE128.py
new file mode 100644
index 00000000..6f04b186
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE128.py
@@ -0,0 +1,145 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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.Util.py3compat import bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr, c_ubyte)
+
+from Crypto.Hash.keccak import _raw_keccak_lib
+
+class SHAKE128_XOF(object):
+ """A SHAKE128 hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+ """
+
+ # ASN.1 Object ID
+ oid = "2.16.840.1.101.3.4.2.11"
+
+ def __init__(self, data=None):
+ state = VoidPointer()
+ result = _raw_keccak_lib.keccak_init(state.address_of(),
+ c_size_t(32),
+ c_ubyte(24))
+ if result:
+ raise ValueError("Error %d while instantiating SHAKE128"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_keccak_lib.keccak_destroy)
+ self._is_squeezing = False
+ self._padding = 0x1F
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._is_squeezing:
+ raise TypeError("You cannot call 'update' after the first 'read'")
+
+ result = _raw_keccak_lib.keccak_absorb(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while updating SHAKE128 state"
+ % result)
+ return self
+
+ def read(self, length):
+ """
+ Compute the next piece of XOF output.
+
+ .. note::
+ You cannot use :meth:`update` anymore after the first call to
+ :meth:`read`.
+
+ Args:
+ length (integer): the amount of bytes this method must return
+
+ :return: the next piece of XOF output (of the given length)
+ :rtype: byte string
+ """
+
+ self._is_squeezing = True
+ bfr = create_string_buffer(length)
+ result = _raw_keccak_lib.keccak_squeeze(self._state.get(),
+ bfr,
+ c_size_t(length),
+ c_ubyte(self._padding))
+ if result:
+ raise ValueError("Error %d while extracting from SHAKE128"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+
+ :return: A hash object of the same type
+ """
+
+ clone = self.new()
+ result = _raw_keccak_lib.keccak_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHAKE128" % result)
+ return clone
+
+ def new(self, data=None):
+ return type(self)(data=data)
+
+
+def new(data=None):
+ """Return a fresh instance of a SHAKE128 object.
+
+ Args:
+ data (bytes/bytearray/memoryview):
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+ Optional.
+
+ :Return: A :class:`SHAKE128_XOF` object
+ """
+
+ return SHAKE128_XOF(data=data)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE128.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE128.pyi
new file mode 100644
index 00000000..de51d8ea
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE128.pyi
@@ -0,0 +1,14 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class SHAKE128_XOF(object):
+ oid: str
+ def __init__(self,
+ data: Optional[Buffer] = ...) -> None: ...
+ def update(self, data: Buffer) -> SHAKE128_XOF: ...
+ def read(self, length: int) -> bytes: ...
+ def copy(self) -> SHAKE128_XOF: ...
+ def new(self, data: Optional[Buffer] = ...) -> SHAKE128_XOF: ...
+
+def new(data: Optional[Buffer] = ...) -> SHAKE128_XOF: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE256.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE256.py
new file mode 100644
index 00000000..c96f0909
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE256.py
@@ -0,0 +1,146 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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.Util.py3compat import bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr, c_ubyte)
+
+from Crypto.Hash.keccak import _raw_keccak_lib
+
+class SHAKE256_XOF(object):
+ """A SHAKE256 hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar oid: ASN.1 Object ID
+ :vartype oid: string
+ """
+
+ # ASN.1 Object ID
+ oid = "2.16.840.1.101.3.4.2.12"
+
+ def __init__(self, data=None):
+ state = VoidPointer()
+ result = _raw_keccak_lib.keccak_init(state.address_of(),
+ c_size_t(64),
+ c_ubyte(24))
+ if result:
+ raise ValueError("Error %d while instantiating SHAKE256"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_keccak_lib.keccak_destroy)
+ self._is_squeezing = False
+ self._padding = 0x1F
+
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._is_squeezing:
+ raise TypeError("You cannot call 'update' after the first 'read'")
+
+ result = _raw_keccak_lib.keccak_absorb(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while updating SHAKE256 state"
+ % result)
+ return self
+
+ def read(self, length):
+ """
+ Compute the next piece of XOF output.
+
+ .. note::
+ You cannot use :meth:`update` anymore after the first call to
+ :meth:`read`.
+
+ Args:
+ length (integer): the amount of bytes this method must return
+
+ :return: the next piece of XOF output (of the given length)
+ :rtype: byte string
+ """
+
+ self._is_squeezing = True
+ bfr = create_string_buffer(length)
+ result = _raw_keccak_lib.keccak_squeeze(self._state.get(),
+ bfr,
+ c_size_t(length),
+ c_ubyte(self._padding))
+ if result:
+ raise ValueError("Error %d while extracting from SHAKE256"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def copy(self):
+ """Return a copy ("clone") of the hash object.
+
+ The copy will have the same internal state as the original hash
+ object.
+
+ :return: A hash object of the same type
+ """
+
+ clone = self.new()
+ result = _raw_keccak_lib.keccak_copy(self._state.get(),
+ clone._state.get())
+ if result:
+ raise ValueError("Error %d while copying SHAKE256" % result)
+ return clone
+
+ def new(self, data=None):
+ return type(self)(data=data)
+
+
+def new(data=None):
+ """Return a fresh instance of a SHAKE256 object.
+
+ Args:
+ data (bytes/bytearray/memoryview):
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+ Optional.
+
+ :Return: A :class:`SHAKE256_XOF` object
+ """
+
+ return SHAKE256_XOF(data=data)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE256.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE256.pyi
new file mode 100644
index 00000000..72eb8985
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/SHAKE256.pyi
@@ -0,0 +1,14 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class SHAKE256_XOF(object):
+ oid: str
+ def __init__(self,
+ data: Optional[Buffer] = ...) -> None: ...
+ def update(self, data: Buffer) -> SHAKE256_XOF: ...
+ def read(self, length: int) -> bytes: ...
+ def copy(self) -> SHAKE256_XOF: ...
+ def new(self, data: Optional[Buffer] = ...) -> SHAKE256_XOF: ...
+
+def new(data: Optional[Buffer] = ...) -> SHAKE256_XOF: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash128.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash128.py
new file mode 100644
index 00000000..a3fa96a1
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash128.py
@@ -0,0 +1,136 @@
+# ===================================================================
+#
+# Copyright (c) 2021, Legrandin
+# 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.Util.py3compat import bord, is_bytes, tobytes
+
+from . import cSHAKE128
+from .cSHAKE128 import _encode_str, _right_encode
+
+
+class TupleHash(object):
+ """A Tuple hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+ """
+
+ def __init__(self, custom, cshake, digest_size):
+
+ self.digest_size = digest_size
+
+ self._cshake = cshake._new(b'', custom, b'TupleHash')
+ self._digest = None
+
+ def update(self, *data):
+ """Authenticate the next tuple of byte strings.
+ TupleHash guarantees the logical separation between each byte string.
+
+ Args:
+ data (bytes/bytearray/memoryview): One or more items to hash.
+ """
+
+ if self._digest is not None:
+ raise TypeError("You cannot call 'update' after 'digest' or 'hexdigest'")
+
+ for item in data:
+ if not is_bytes(item):
+ raise TypeError("You can only call 'update' on bytes" )
+ self._cshake.update(_encode_str(item))
+
+ return self
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the tuple of byte strings.
+
+ :return: The hash digest. Binary form.
+ :rtype: byte string
+ """
+
+ if self._digest is None:
+ self._cshake.update(_right_encode(self.digest_size * 8))
+ self._digest = self._cshake.read(self.digest_size)
+
+ return self._digest
+
+ def hexdigest(self):
+ """Return the **printable** digest of the tuple of byte strings.
+
+ :return: The hash digest. Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in tuple(self.digest())])
+
+ def new(self, **kwargs):
+ """Return a new instance of a TupleHash object.
+ See :func:`new`.
+ """
+
+ if "digest_bytes" not in kwargs and "digest_bits" not in kwargs:
+ kwargs["digest_bytes"] = self.digest_size
+
+ return new(**kwargs)
+
+
+def new(**kwargs):
+ """Create a new TupleHash128 object.
+
+ Args:
+ digest_bytes (integer):
+ Optional. The size of the digest, in bytes.
+ Default is 64. Minimum is 8.
+ digest_bits (integer):
+ Optional and alternative to ``digest_bytes``.
+ The size of the digest, in bits (and in steps of 8).
+ Default is 512. Minimum is 64.
+ custom (bytes):
+ Optional.
+ A customization bytestring (``S`` in SP 800-185).
+
+ :Return: A :class:`TupleHash` object
+ """
+
+ digest_bytes = kwargs.pop("digest_bytes", None)
+ digest_bits = kwargs.pop("digest_bits", None)
+ if None not in (digest_bytes, digest_bits):
+ raise TypeError("Only one digest parameter must be provided")
+ if (None, None) == (digest_bytes, digest_bits):
+ digest_bytes = 64
+ if digest_bytes is not None:
+ if digest_bytes < 8:
+ raise ValueError("'digest_bytes' must be at least 8")
+ else:
+ if digest_bits < 64 or digest_bits % 8:
+ raise ValueError("'digest_bytes' must be at least 64 "
+ "in steps of 8")
+ digest_bytes = digest_bits // 8
+
+ custom = kwargs.pop("custom", b'')
+
+ return TupleHash(custom, cSHAKE128, digest_bytes)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash128.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash128.pyi
new file mode 100644
index 00000000..2e0ea833
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash128.pyi
@@ -0,0 +1,22 @@
+from typing import Any, Union, List, Tuple
+from types import ModuleType
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class TupleHash(object):
+ digest_size: int
+ def __init__(self,
+ custom: bytes,
+ cshake: ModuleType,
+ digest_size: int) -> None: ...
+ def update(self, *data: Buffer) -> TupleHash: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def new(self,
+ digest_bytes: int = ...,
+ digest_bits: int = ...,
+ custom: int = ...) -> TupleHash: ...
+
+def new(digest_bytes: int = ...,
+ digest_bits: int = ...,
+ custom: int = ...) -> TupleHash: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash256.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash256.py
new file mode 100644
index 00000000..40a824a9
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash256.py
@@ -0,0 +1,70 @@
+# ===================================================================
+#
+# Copyright (c) 2021, Legrandin
+# 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 . import cSHAKE256
+from .TupleHash128 import TupleHash
+
+
+def new(**kwargs):
+ """Create a new TupleHash256 object.
+
+ Args:
+ digest_bytes (integer):
+ Optional. The size of the digest, in bytes.
+ Default is 64. Minimum is 8.
+ digest_bits (integer):
+ Optional and alternative to ``digest_bytes``.
+ The size of the digest, in bits (and in steps of 8).
+ Default is 512. Minimum is 64.
+ custom (bytes):
+ Optional.
+ A customization bytestring (``S`` in SP 800-185).
+
+ :Return: A :class:`TupleHash` object
+ """
+
+ digest_bytes = kwargs.pop("digest_bytes", None)
+ digest_bits = kwargs.pop("digest_bits", None)
+ if None not in (digest_bytes, digest_bits):
+ raise TypeError("Only one digest parameter must be provided")
+ if (None, None) == (digest_bytes, digest_bits):
+ digest_bytes = 64
+ if digest_bytes is not None:
+ if digest_bytes < 8:
+ raise ValueError("'digest_bytes' must be at least 8")
+ else:
+ if digest_bits < 64 or digest_bits % 8:
+ raise ValueError("'digest_bytes' must be at least 64 "
+ "in steps of 8")
+ digest_bytes = digest_bits // 8
+
+ custom = kwargs.pop("custom", b'')
+
+ return TupleHash(custom, cSHAKE256, digest_bytes)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash256.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash256.pyi
new file mode 100644
index 00000000..82d943f0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TupleHash256.pyi
@@ -0,0 +1,5 @@
+from .TupleHash128 import TupleHash
+
+def new(digest_bytes: int = ...,
+ digest_bits: int = ...,
+ custom: int = ...) -> TupleHash: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE128.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE128.py
new file mode 100644
index 00000000..ab3e4e49
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE128.py
@@ -0,0 +1,112 @@
+from Crypto.Util._raw_api import (VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr, c_ubyte)
+
+from Crypto.Util.number import long_to_bytes
+from Crypto.Util.py3compat import bchr
+
+from .keccak import _raw_keccak_lib
+
+
+class TurboSHAKE(object):
+ """A TurboSHAKE hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+ """
+
+ def __init__(self, capacity, domain_separation, data):
+
+ state = VoidPointer()
+ result = _raw_keccak_lib.keccak_init(state.address_of(),
+ c_size_t(capacity),
+ c_ubyte(12)) # Reduced number of rounds
+ if result:
+ raise ValueError("Error %d while instantiating TurboSHAKE"
+ % result)
+ self._state = SmartPointer(state.get(), _raw_keccak_lib.keccak_destroy)
+
+ self._is_squeezing = False
+ self._capacity = capacity
+ self._domain = domain_separation
+
+ if data:
+ self.update(data)
+
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._is_squeezing:
+ raise TypeError("You cannot call 'update' after the first 'read'")
+
+ result = _raw_keccak_lib.keccak_absorb(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while updating TurboSHAKE state"
+ % result)
+ return self
+
+ def read(self, length):
+ """
+ Compute the next piece of XOF output.
+
+ .. note::
+ You cannot use :meth:`update` anymore after the first call to
+ :meth:`read`.
+
+ Args:
+ length (integer): the amount of bytes this method must return
+
+ :return: the next piece of XOF output (of the given length)
+ :rtype: byte string
+ """
+
+ self._is_squeezing = True
+ bfr = create_string_buffer(length)
+ result = _raw_keccak_lib.keccak_squeeze(self._state.get(),
+ bfr,
+ c_size_t(length),
+ c_ubyte(self._domain))
+ if result:
+ raise ValueError("Error %d while extracting from TurboSHAKE"
+ % result)
+
+ return get_raw_buffer(bfr)
+
+ def new(self, data=None):
+ return type(self)(self._capacity, self._domain, data)
+
+ def _reset(self):
+ result = _raw_keccak_lib.keccak_reset(self._state.get())
+ if result:
+ raise ValueError("Error %d while resetting TurboSHAKE state"
+ % result)
+ self._is_squeezing = False
+
+
+def new(**kwargs):
+ """Create a new TurboSHAKE128 object.
+
+ Args:
+ domain (integer):
+ Optional - A domain separation byte, between 0x01 and 0x7F.
+ The default value is 0x1F.
+ data (bytes/bytearray/memoryview):
+ Optional - The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+
+ :Return: A :class:`TurboSHAKE` object
+ """
+
+ domain_separation = kwargs.get('domain', 0x1F)
+ if not (0x01 <= domain_separation <= 0x7F):
+ raise ValueError("Incorrect domain separation value (%d)" %
+ domain_separation)
+ data = kwargs.get('data')
+ return TurboSHAKE(32, domain_separation, data=data)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE128.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE128.pyi
new file mode 100644
index 00000000..d74c9c08
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE128.pyi
@@ -0,0 +1,17 @@
+from typing import Union, Optional
+from typing_extensions import TypedDict, Unpack, NotRequired
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class TurboSHAKE(object):
+
+ def __init__(self, capacity: int, domain_separation: int, data: Union[Buffer, None]) -> None: ...
+ def update(self, data: Buffer) -> TurboSHAKE : ...
+ def read(self, length: int) -> bytes: ...
+ def new(self, data: Optional[Buffer]=None) -> TurboSHAKE: ...
+
+class Args(TypedDict):
+ domain: NotRequired[int]
+ data: NotRequired[Buffer]
+
+def new(**kwargs: Unpack[Args]) -> TurboSHAKE: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE256.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE256.py
new file mode 100644
index 00000000..ce27a485
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE256.py
@@ -0,0 +1,22 @@
+from .TurboSHAKE128 import TurboSHAKE
+
+def new(**kwargs):
+ """Create a new TurboSHAKE256 object.
+
+ Args:
+ domain (integer):
+ Optional - A domain separation byte, between 0x01 and 0x7F.
+ The default value is 0x1F.
+ data (bytes/bytearray/memoryview):
+ Optional - The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+
+ :Return: A :class:`TurboSHAKE` object
+ """
+
+ domain_separation = kwargs.get('domain', 0x1F)
+ if not (0x01 <= domain_separation <= 0x7F):
+ raise ValueError("Incorrect domain separation value (%d)" %
+ domain_separation)
+ data = kwargs.get('data')
+ return TurboSHAKE(64, domain_separation, data=data)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE256.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE256.pyi
new file mode 100644
index 00000000..561e946c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/TurboSHAKE256.pyi
@@ -0,0 +1,12 @@
+from typing import Union
+from typing_extensions import TypedDict, Unpack, NotRequired
+
+from .TurboSHAKE128 import TurboSHAKE
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class Args(TypedDict):
+ domain: NotRequired[int]
+ data: NotRequired[Buffer]
+
+def new(**kwargs: Unpack[Args]) -> TurboSHAKE: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_BLAKE2b.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_BLAKE2b.abi3.so
new file mode 100755
index 00000000..ad5b9306
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_BLAKE2b.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_BLAKE2s.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_BLAKE2s.abi3.so
new file mode 100755
index 00000000..e6d48671
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_BLAKE2s.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_MD2.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_MD2.abi3.so
new file mode 100755
index 00000000..49f4f662
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_MD2.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_MD4.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_MD4.abi3.so
new file mode 100755
index 00000000..b54c23ac
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_MD4.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_MD5.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_MD5.abi3.so
new file mode 100755
index 00000000..2eb506f9
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_MD5.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_RIPEMD160.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_RIPEMD160.abi3.so
new file mode 100755
index 00000000..369f91b4
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_RIPEMD160.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA1.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA1.abi3.so
new file mode 100755
index 00000000..48adbccb
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA1.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA224.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA224.abi3.so
new file mode 100755
index 00000000..f1ee58e6
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA224.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA256.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA256.abi3.so
new file mode 100755
index 00000000..ebe2ae02
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA256.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA384.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA384.abi3.so
new file mode 100755
index 00000000..5075481f
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA384.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA512.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA512.abi3.so
new file mode 100755
index 00000000..3a34d229
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_SHA512.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/__init__.py
new file mode 100644
index 00000000..80446e4f
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/__init__.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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__ = ['HMAC', 'MD2', 'MD4', 'MD5', 'RIPEMD160', 'SHA1',
+ 'SHA224', 'SHA256', 'SHA384', 'SHA512',
+ 'SHA3_224', 'SHA3_256', 'SHA3_384', 'SHA3_512',
+ 'CMAC', 'Poly1305',
+ 'cSHAKE128', 'cSHAKE256', 'KMAC128', 'KMAC256',
+ 'TupleHash128', 'TupleHash256', 'KangarooTwelve',
+ 'TurboSHAKE128', 'TurboSHAKE256']
+
+def new(name):
+ """Return a new hash instance, based on its name or
+ on its ASN.1 Object ID"""
+
+ name = name.upper()
+ if name in ("1.3.14.3.2.26", "SHA1", "SHA-1"):
+ from . import SHA1
+ return SHA1.new()
+ if name in ("2.16.840.1.101.3.4.2.4", "SHA224", "SHA-224"):
+ from . import SHA224
+ return SHA224.new()
+ if name in ("2.16.840.1.101.3.4.2.1", "SHA256", "SHA-256"):
+ from . import SHA256
+ return SHA256.new()
+ if name in ("2.16.840.1.101.3.4.2.2", "SHA384", "SHA-384"):
+ from . import SHA384
+ return SHA384.new()
+ if name in ("2.16.840.1.101.3.4.2.3", "SHA512", "SHA-512"):
+ from . import SHA512
+ return SHA512.new()
+ if name in ("2.16.840.1.101.3.4.2.5", "SHA512-224", "SHA-512-224"):
+ from . import SHA512
+ return SHA512.new(truncate='224')
+ if name in ("2.16.840.1.101.3.4.2.6", "SHA512-256", "SHA-512-256"):
+ from . import SHA512
+ return SHA512.new(truncate='256')
+ if name in ("2.16.840.1.101.3.4.2.7", "SHA3-224", "SHA-3-224"):
+ from . import SHA3_224
+ return SHA3_224.new()
+ if name in ("2.16.840.1.101.3.4.2.8", "SHA3-256", "SHA-3-256"):
+ from . import SHA3_256
+ return SHA3_256.new()
+ if name in ("2.16.840.1.101.3.4.2.9", "SHA3-384", "SHA-3-384"):
+ from . import SHA3_384
+ return SHA3_384.new()
+ if name in ("2.16.840.1.101.3.4.2.10", "SHA3-512", "SHA-3-512"):
+ from . import SHA3_512
+ return SHA3_512.new()
+ else:
+ raise ValueError("Unknown hash %s" % str(name))
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/__init__.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/__init__.pyi
new file mode 100644
index 00000000..36ad48d1
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/__init__.pyi
@@ -0,0 +1,57 @@
+from typing import overload
+from typing_extensions import Literal
+
+from Crypto.Hash.SHA1 import SHA1Hash
+from Crypto.Hash.SHA224 import SHA224Hash
+from Crypto.Hash.SHA256 import SHA256Hash
+from Crypto.Hash.SHA384 import SHA384Hash
+from Crypto.Hash.SHA512 import SHA512Hash
+from Crypto.Hash.SHA3_224 import SHA3_224_Hash
+from Crypto.Hash.SHA3_256 import SHA3_256_Hash
+from Crypto.Hash.SHA3_384 import SHA3_384_Hash
+from Crypto.Hash.SHA3_512 import SHA3_512_Hash
+
+@overload
+def new(name: Literal["1.3.14.3.2.26"]) -> SHA1Hash: ...
+@overload
+def new(name: Literal["SHA1"]) -> SHA1Hash: ...
+@overload
+def new(name: Literal["2.16.840.1.101.3.4.2.4"]) -> SHA224Hash: ...
+@overload
+def new(name: Literal["SHA224"]) -> SHA224Hash: ...
+@overload
+def new(name: Literal["2.16.840.1.101.3.4.2.1"]) -> SHA256Hash: ...
+@overload
+def new(name: Literal["SHA256"]) -> SHA256Hash: ...
+@overload
+def new(name: Literal["2.16.840.1.101.3.4.2.2"]) -> SHA384Hash: ...
+@overload
+def new(name: Literal["SHA384"]) -> SHA384Hash: ...
+@overload
+def new(name: Literal["2.16.840.1.101.3.4.2.3"]) -> SHA512Hash: ...
+@overload
+def new(name: Literal["SHA512"]) -> SHA512Hash: ...
+@overload
+def new(name: Literal["2.16.840.1.101.3.4.2.5"]) -> SHA512Hash: ...
+@overload
+def new(name: Literal["SHA512-224"]) -> SHA512Hash: ...
+@overload
+def new(name: Literal["2.16.840.1.101.3.4.2.6"]) -> SHA512Hash: ...
+@overload
+def new(name: Literal["SHA512-256"]) -> SHA512Hash: ...
+@overload
+def new(name: Literal["2.16.840.1.101.3.4.2.7"]) -> SHA3_224_Hash: ...
+@overload
+def new(name: Literal["SHA3-224"]) -> SHA3_224_Hash: ...
+@overload
+def new(name: Literal["2.16.840.1.101.3.4.2.8"]) -> SHA3_256_Hash: ...
+@overload
+def new(name: Literal["SHA3-256"]) -> SHA3_256_Hash: ...
+@overload
+def new(name: Literal["2.16.840.1.101.3.4.2.9"]) -> SHA3_384_Hash: ...
+@overload
+def new(name: Literal["SHA3-384"]) -> SHA3_384_Hash: ...
+@overload
+def new(name: Literal["2.16.840.1.101.3.4.2.10"]) -> SHA3_512_Hash: ...
+@overload
+def new(name: Literal["SHA3-512"]) -> SHA3_512_Hash: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_ghash_portable.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_ghash_portable.abi3.so
new file mode 100755
index 00000000..4f5f0301
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_ghash_portable.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_keccak.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_keccak.abi3.so
new file mode 100755
index 00000000..76d32dcd
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_keccak.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_poly1305.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_poly1305.abi3.so
new file mode 100755
index 00000000..8f2a5d98
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/_poly1305.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE128.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE128.py
new file mode 100644
index 00000000..3907975f
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE128.py
@@ -0,0 +1,187 @@
+# ===================================================================
+#
+# Copyright (c) 2021, Legrandin
+# 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.Util.py3compat import bchr, concat_buffers
+
+from Crypto.Util._raw_api import (VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr, c_ubyte)
+
+from Crypto.Util.number import long_to_bytes
+
+from Crypto.Hash.keccak import _raw_keccak_lib
+
+
+def _left_encode(x):
+ """Left encode function as defined in NIST SP 800-185"""
+
+ assert (x < (1 << 2040) and x >= 0)
+
+ # Get number of bytes needed to represent this integer.
+ num = 1 if x == 0 else (x.bit_length() + 7) // 8
+
+ return bchr(num) + long_to_bytes(x)
+
+
+def _right_encode(x):
+ """Right encode function as defined in NIST SP 800-185"""
+
+ assert (x < (1 << 2040) and x >= 0)
+
+ # Get number of bytes needed to represent this integer.
+ num = 1 if x == 0 else (x.bit_length() + 7) // 8
+
+ return long_to_bytes(x) + bchr(num)
+
+
+def _encode_str(x):
+ """Encode string function as defined in NIST SP 800-185"""
+
+ bitlen = len(x) * 8
+ if bitlen >= (1 << 2040):
+ raise ValueError("String too large to encode in cSHAKE")
+
+ return concat_buffers(_left_encode(bitlen), x)
+
+
+def _bytepad(x, length):
+ """Zero pad byte string as defined in NIST SP 800-185"""
+
+ to_pad = concat_buffers(_left_encode(length), x)
+
+ # Note: this implementation works with byte aligned strings,
+ # hence no additional bit padding is needed at this point.
+ npad = (length - len(to_pad) % length) % length
+
+ return to_pad + b'\x00' * npad
+
+
+class cSHAKE_XOF(object):
+ """A cSHAKE hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+ """
+
+ def __init__(self, data, custom, capacity, function):
+ state = VoidPointer()
+
+ if custom or function:
+ prefix_unpad = _encode_str(function) + _encode_str(custom)
+ prefix = _bytepad(prefix_unpad, (1600 - capacity)//8)
+ self._padding = 0x04
+ else:
+ prefix = None
+ self._padding = 0x1F # for SHAKE
+
+ result = _raw_keccak_lib.keccak_init(state.address_of(),
+ c_size_t(capacity//8),
+ c_ubyte(24))
+ if result:
+ raise ValueError("Error %d while instantiating cSHAKE"
+ % result)
+ self._state = SmartPointer(state.get(),
+ _raw_keccak_lib.keccak_destroy)
+ self._is_squeezing = False
+
+ if prefix:
+ self.update(prefix)
+
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._is_squeezing:
+ raise TypeError("You cannot call 'update' after the first 'read'")
+
+ result = _raw_keccak_lib.keccak_absorb(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while updating %s state"
+ % (result, self.name))
+ return self
+
+ def read(self, length):
+ """
+ Compute the next piece of XOF output.
+
+ .. note::
+ You cannot use :meth:`update` anymore after the first call to
+ :meth:`read`.
+
+ Args:
+ length (integer): the amount of bytes this method must return
+
+ :return: the next piece of XOF output (of the given length)
+ :rtype: byte string
+ """
+
+ self._is_squeezing = True
+ bfr = create_string_buffer(length)
+ result = _raw_keccak_lib.keccak_squeeze(self._state.get(),
+ bfr,
+ c_size_t(length),
+ c_ubyte(self._padding))
+ if result:
+ raise ValueError("Error %d while extracting from %s"
+ % (result, self.name))
+
+ return get_raw_buffer(bfr)
+
+
+def _new(data, custom, function):
+ # Use Keccak[256]
+ return cSHAKE_XOF(data, custom, 256, function)
+
+
+def new(data=None, custom=None):
+ """Return a fresh instance of a cSHAKE128 object.
+
+ Args:
+ data (bytes/bytearray/memoryview):
+ Optional.
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+ custom (bytes):
+ Optional.
+ A customization bytestring (``S`` in SP 800-185).
+
+ :Return: A :class:`cSHAKE_XOF` object
+ """
+
+ # Use Keccak[256]
+ return cSHAKE_XOF(data, custom, 256, b'')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE128.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE128.pyi
new file mode 100644
index 00000000..1452feaf
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE128.pyi
@@ -0,0 +1,14 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class cSHAKE_XOF(object):
+ def __init__(self,
+ data: Optional[Buffer] = ...,
+ function: Optional[bytes] = ...,
+ custom: Optional[bytes] = ...) -> None: ...
+ def update(self, data: Buffer) -> cSHAKE_XOF: ...
+ def read(self, length: int) -> bytes: ...
+
+def new(data: Optional[Buffer] = ...,
+ custom: Optional[Buffer] = ...) -> cSHAKE_XOF: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE256.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE256.py
new file mode 100644
index 00000000..b3b31d6d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE256.py
@@ -0,0 +1,56 @@
+# ===================================================================
+#
+# Copyright (c) 2021, Legrandin
+# 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.Util._raw_api import c_size_t
+from Crypto.Hash.cSHAKE128 import cSHAKE_XOF
+
+
+def _new(data, custom, function):
+ # Use Keccak[512]
+ return cSHAKE_XOF(data, custom, 512, function)
+
+
+def new(data=None, custom=None):
+ """Return a fresh instance of a cSHAKE256 object.
+
+ Args:
+ data (bytes/bytearray/memoryview):
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`update`.
+ Optional.
+ custom (bytes):
+ Optional.
+ A customization bytestring (``S`` in SP 800-185).
+
+ :Return: A :class:`cSHAKE_XOF` object
+ """
+
+ # Use Keccak[512]
+ return cSHAKE_XOF(data, custom, 512, b'')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE256.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE256.pyi
new file mode 100644
index 00000000..205b816a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/cSHAKE256.pyi
@@ -0,0 +1,8 @@
+from typing import Union, Optional
+
+from Crypto.Hash.cSHAKE128 import cSHAKE_XOF
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+def new(data: Optional[Buffer] = ...,
+ custom: Optional[Buffer] = ...) -> cSHAKE_XOF: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/keccak.py b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/keccak.py
new file mode 100644
index 00000000..f3f8bb5a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/keccak.py
@@ -0,0 +1,181 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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.Util.py3compat import bord
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ VoidPointer, SmartPointer,
+ create_string_buffer,
+ get_raw_buffer, c_size_t,
+ c_uint8_ptr, c_ubyte)
+
+_raw_keccak_lib = load_pycryptodome_raw_lib("Crypto.Hash._keccak",
+ """
+ int keccak_init(void **state,
+ size_t capacity_bytes,
+ uint8_t rounds);
+ int keccak_destroy(void *state);
+ int keccak_absorb(void *state,
+ const uint8_t *in,
+ size_t len);
+ int keccak_squeeze(const void *state,
+ uint8_t *out,
+ size_t len,
+ uint8_t padding);
+ int keccak_digest(void *state,
+ uint8_t *digest,
+ size_t len,
+ uint8_t padding);
+ int keccak_copy(const void *src, void *dst);
+ int keccak_reset(void *state);
+ """)
+
+class Keccak_Hash(object):
+ """A Keccak hash object.
+ Do not instantiate directly.
+ Use the :func:`new` function.
+
+ :ivar digest_size: the size in bytes of the resulting hash
+ :vartype digest_size: integer
+ """
+
+ def __init__(self, data, digest_bytes, update_after_digest):
+ # The size of the resulting hash in bytes.
+ self.digest_size = digest_bytes
+
+ self._update_after_digest = update_after_digest
+ self._digest_done = False
+ self._padding = 0x01
+
+ state = VoidPointer()
+ result = _raw_keccak_lib.keccak_init(state.address_of(),
+ c_size_t(self.digest_size * 2),
+ c_ubyte(24))
+ if result:
+ raise ValueError("Error %d while instantiating keccak" % result)
+ self._state = SmartPointer(state.get(),
+ _raw_keccak_lib.keccak_destroy)
+ if data:
+ self.update(data)
+
+ def update(self, data):
+ """Continue hashing of a message by consuming the next chunk of data.
+
+ Args:
+ data (byte string/byte array/memoryview): The next chunk of the message being hashed.
+ """
+
+ if self._digest_done and not self._update_after_digest:
+ raise TypeError("You can only call 'digest' or 'hexdigest' on this object")
+
+ result = _raw_keccak_lib.keccak_absorb(self._state.get(),
+ c_uint8_ptr(data),
+ c_size_t(len(data)))
+ if result:
+ raise ValueError("Error %d while updating keccak" % result)
+ return self
+
+ def digest(self):
+ """Return the **binary** (non-printable) digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Binary form.
+ :rtype: byte string
+ """
+
+ self._digest_done = True
+ bfr = create_string_buffer(self.digest_size)
+ result = _raw_keccak_lib.keccak_digest(self._state.get(),
+ bfr,
+ c_size_t(self.digest_size),
+ c_ubyte(self._padding))
+ if result:
+ raise ValueError("Error %d while squeezing keccak" % result)
+
+ return get_raw_buffer(bfr)
+
+ def hexdigest(self):
+ """Return the **printable** digest of the message that has been hashed so far.
+
+ :return: The hash digest, computed over the data processed so far.
+ Hexadecimal encoded.
+ :rtype: string
+ """
+
+ return "".join(["%02x" % bord(x) for x in self.digest()])
+
+ def new(self, **kwargs):
+ """Create a fresh Keccak hash object."""
+
+ if "digest_bytes" not in kwargs and "digest_bits" not in kwargs:
+ kwargs["digest_bytes"] = self.digest_size
+
+ return new(**kwargs)
+
+
+def new(**kwargs):
+ """Create a new hash object.
+
+ Args:
+ data (bytes/bytearray/memoryview):
+ The very first chunk of the message to hash.
+ It is equivalent to an early call to :meth:`Keccak_Hash.update`.
+ digest_bytes (integer):
+ The size of the digest, in bytes (28, 32, 48, 64).
+ digest_bits (integer):
+ The size of the digest, in bits (224, 256, 384, 512).
+ update_after_digest (boolean):
+ Whether :meth:`Keccak.digest` can be followed by another
+ :meth:`Keccak.update` (default: ``False``).
+
+ :Return: A :class:`Keccak_Hash` hash object
+ """
+
+ data = kwargs.pop("data", None)
+ update_after_digest = kwargs.pop("update_after_digest", False)
+
+ digest_bytes = kwargs.pop("digest_bytes", None)
+ digest_bits = kwargs.pop("digest_bits", None)
+ if None not in (digest_bytes, digest_bits):
+ raise TypeError("Only one digest parameter must be provided")
+ if (None, None) == (digest_bytes, digest_bits):
+ raise TypeError("Digest size (bits, bytes) not provided")
+ if digest_bytes is not None:
+ if digest_bytes not in (28, 32, 48, 64):
+ raise ValueError("'digest_bytes' must be: 28, 32, 48 or 64")
+ else:
+ if digest_bits not in (224, 256, 384, 512):
+ raise ValueError("'digest_bytes' must be: 224, 256, 384 or 512")
+ digest_bytes = digest_bits // 8
+
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ return Keccak_Hash(data, digest_bytes, update_after_digest)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Hash/keccak.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/keccak.pyi
new file mode 100644
index 00000000..844d2565
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Hash/keccak.pyi
@@ -0,0 +1,23 @@
+from typing import Union, Any
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+class Keccak_Hash(object):
+ digest_size: int
+ def __init__(self,
+ data: Buffer,
+ digest_bytes: int,
+ update_after_digest: bool) -> None: ...
+ def update(self, data: Buffer) -> Keccak_Hash: ...
+ def digest(self) -> bytes: ...
+ def hexdigest(self) -> str: ...
+ def new(self,
+ data: Buffer = ...,
+ digest_bytes: int = ...,
+ digest_bits: int = ...,
+ update_after_digest: bool = ...) -> Keccak_Hash: ...
+
+def new(data: Buffer = ...,
+ digest_bytes: int = ...,
+ digest_bits: int = ...,
+ update_after_digest: bool = ...) -> Keccak_Hash: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/IO/PEM.py b/backend/venv/lib/python3.9/site-packages/Crypto/IO/PEM.py
new file mode 100644
index 00000000..9b8b0710
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/IO/PEM.py
@@ -0,0 +1,191 @@
+#
+# Util/PEM.py : Privacy Enhanced Mail utilities
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+__all__ = ['encode', 'decode']
+
+import re
+from binascii import a2b_base64, b2a_base64, hexlify, unhexlify
+
+from Crypto.Hash import MD5
+from Crypto.Util.Padding import pad, unpad
+from Crypto.Cipher import DES, DES3, AES
+from Crypto.Protocol.KDF import PBKDF1
+from Crypto.Random import get_random_bytes
+from Crypto.Util.py3compat import tobytes, tostr
+
+
+def encode(data, marker, passphrase=None, randfunc=None):
+ """Encode a piece of binary data into PEM format.
+
+ Args:
+ data (byte string):
+ The piece of binary data to encode.
+ marker (string):
+ The marker for the PEM block (e.g. "PUBLIC KEY").
+ Note that there is no official master list for all allowed markers.
+ Still, you can refer to the OpenSSL_ source code.
+ passphrase (byte string):
+ If given, the PEM block will be encrypted. The key is derived from
+ the passphrase.
+ randfunc (callable):
+ Random number generation function; it accepts an integer N and returns
+ a byte string of random data, N bytes long. If not given, a new one is
+ instantiated.
+
+ Returns:
+ The PEM block, as a string.
+
+ .. _OpenSSL: https://github.com/openssl/openssl/blob/master/include/openssl/pem.h
+ """
+
+ if randfunc is None:
+ randfunc = get_random_bytes
+
+ out = "-----BEGIN %s-----\n" % marker
+ if passphrase:
+ # We only support 3DES for encryption
+ salt = randfunc(8)
+ key = PBKDF1(passphrase, salt, 16, 1, MD5)
+ key += PBKDF1(key + passphrase, salt, 8, 1, MD5)
+ objenc = DES3.new(key, DES3.MODE_CBC, salt)
+ out += "Proc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC,%s\n\n" %\
+ tostr(hexlify(salt).upper())
+ # Encrypt with PKCS#7 padding
+ data = objenc.encrypt(pad(data, objenc.block_size))
+ elif passphrase is not None:
+ raise ValueError("Empty password")
+
+ # Each BASE64 line can take up to 64 characters (=48 bytes of data)
+ # b2a_base64 adds a new line character!
+ chunks = [tostr(b2a_base64(data[i:i + 48]))
+ for i in range(0, len(data), 48)]
+ out += "".join(chunks)
+ out += "-----END %s-----" % marker
+ return out
+
+
+def _EVP_BytesToKey(data, salt, key_len):
+ d = [ b'' ]
+ m = (key_len + 15 ) // 16
+ for _ in range(m):
+ nd = MD5.new(d[-1] + data + salt).digest()
+ d.append(nd)
+ return b"".join(d)[:key_len]
+
+
+def decode(pem_data, passphrase=None):
+ """Decode a PEM block into binary.
+
+ Args:
+ pem_data (string):
+ The PEM block.
+ passphrase (byte string):
+ If given and the PEM block is encrypted,
+ the key will be derived from the passphrase.
+
+ Returns:
+ A tuple with the binary data, the marker string, and a boolean to
+ indicate if decryption was performed.
+
+ Raises:
+ ValueError: if decoding fails, if the PEM file is encrypted and no passphrase has
+ been provided or if the passphrase is incorrect.
+ """
+
+ # Verify Pre-Encapsulation Boundary
+ r = re.compile(r"\s*-----BEGIN (.*)-----\s+")
+ m = r.match(pem_data)
+ if not m:
+ raise ValueError("Not a valid PEM pre boundary")
+ marker = m.group(1)
+
+ # Verify Post-Encapsulation Boundary
+ r = re.compile(r"-----END (.*)-----\s*$")
+ m = r.search(pem_data)
+ if not m or m.group(1) != marker:
+ raise ValueError("Not a valid PEM post boundary")
+
+ # Removes spaces and slit on lines
+ lines = pem_data.replace(" ", '').split()
+ if len(lines) < 3:
+ raise ValueError("A PEM file must have at least 3 lines")
+
+ # Decrypts, if necessary
+ if lines[1].startswith('Proc-Type:4,ENCRYPTED'):
+ if not passphrase:
+ raise ValueError("PEM is encrypted, but no passphrase available")
+ DEK = lines[2].split(':')
+ if len(DEK) != 2 or DEK[0] != 'DEK-Info':
+ raise ValueError("PEM encryption format not supported.")
+ algo, salt = DEK[1].split(',')
+ salt = unhexlify(tobytes(salt))
+
+ padding = True
+
+ if algo == "DES-CBC":
+ key = _EVP_BytesToKey(passphrase, salt, 8)
+ objdec = DES.new(key, DES.MODE_CBC, salt)
+ elif algo == "DES-EDE3-CBC":
+ key = _EVP_BytesToKey(passphrase, salt, 24)
+ objdec = DES3.new(key, DES3.MODE_CBC, salt)
+ elif algo == "AES-128-CBC":
+ key = _EVP_BytesToKey(passphrase, salt[:8], 16)
+ objdec = AES.new(key, AES.MODE_CBC, salt)
+ elif algo == "AES-192-CBC":
+ key = _EVP_BytesToKey(passphrase, salt[:8], 24)
+ objdec = AES.new(key, AES.MODE_CBC, salt)
+ elif algo == "AES-256-CBC":
+ key = _EVP_BytesToKey(passphrase, salt[:8], 32)
+ objdec = AES.new(key, AES.MODE_CBC, salt)
+ elif algo.lower() == "id-aes256-gcm":
+ key = _EVP_BytesToKey(passphrase, salt[:8], 32)
+ objdec = AES.new(key, AES.MODE_GCM, nonce=salt)
+ padding = False
+ else:
+ raise ValueError("Unsupport PEM encryption algorithm (%s)." % algo)
+ lines = lines[2:]
+ else:
+ objdec = None
+
+ # Decode body
+ data = a2b_base64(''.join(lines[1:-1]))
+ enc_flag = False
+ if objdec:
+ if padding:
+ data = unpad(objdec.decrypt(data), objdec.block_size)
+ else:
+ # There is no tag, so we don't use decrypt_and_verify
+ data = objdec.decrypt(data)
+ enc_flag = True
+
+ return (data, marker, enc_flag)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/IO/PEM.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/IO/PEM.pyi
new file mode 100644
index 00000000..2e324c45
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/IO/PEM.pyi
@@ -0,0 +1,10 @@
+from typing import Tuple, Optional, Callable
+
+def encode(data: bytes,
+ marke: str,
+ passphrase: Optional[bytes] = ...,
+ randfunc: Optional[Callable[[int],bytes]] = ...) -> str: ...
+
+
+def decode(pem_data: str,
+ passphrase: Optional[bytes] = ...) -> Tuple[bytes, str, bool]: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/IO/PKCS8.py b/backend/venv/lib/python3.9/site-packages/Crypto/IO/PKCS8.py
new file mode 100644
index 00000000..c1b79a28
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/IO/PKCS8.py
@@ -0,0 +1,226 @@
+#
+# PublicKey/PKCS8.py : PKCS#8 functions
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.Util.py3compat import *
+
+from Crypto.Util.asn1 import (
+ DerNull,
+ DerSequence,
+ DerObjectId,
+ DerOctetString,
+ )
+
+from Crypto.IO._PBES import PBES1, PBES2, PbesError
+
+
+__all__ = ['wrap', 'unwrap']
+
+
+def wrap(private_key, key_oid, passphrase=None, protection=None,
+ prot_params=None, key_params=DerNull(), randfunc=None):
+ """Wrap a private key into a PKCS#8 blob (clear or encrypted).
+
+ Args:
+
+ private_key (bytes):
+ The private key encoded in binary form. The actual encoding is
+ algorithm specific. In most cases, it is DER.
+
+ key_oid (string):
+ The object identifier (OID) of the private key to wrap.
+ It is a dotted string, like ``'1.2.840.113549.1.1.1'`` (for RSA keys)
+ or ``'1.2.840.10045.2.1'`` (for ECC keys).
+
+ Keyword Args:
+
+ passphrase (bytes or string):
+ The secret passphrase from which the wrapping key is derived.
+ Set it only if encryption is required.
+
+ protection (string):
+ The identifier of the algorithm to use for securely wrapping the key.
+ Refer to :ref:`the encryption parameters` .
+ The default value is ``'PBKDF2WithHMAC-SHA1AndDES-EDE3-CBC'``.
+
+ prot_params (dictionary):
+ Parameters for the key derivation function (KDF).
+ Refer to :ref:`the encryption parameters` .
+
+ key_params (DER object or None):
+ The ``parameters`` field to use in the ``AlgorithmIdentifier``
+ SEQUENCE. If ``None``, no ``parameters`` field will be added.
+ By default, the ASN.1 type ``NULL`` is used.
+
+ randfunc (callable):
+ Random number generation function; it should accept a single integer
+ N and return a string of random data, N bytes long.
+ If not specified, a new RNG will be instantiated
+ from :mod:`Crypto.Random`.
+
+ Returns:
+ bytes: The PKCS#8-wrapped private key (possibly encrypted).
+ """
+
+ #
+ # PrivateKeyInfo ::= SEQUENCE {
+ # version Version,
+ # privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
+ # privateKey PrivateKey,
+ # attributes [0] IMPLICIT Attributes OPTIONAL
+ # }
+ #
+ if key_params is None:
+ algorithm = DerSequence([DerObjectId(key_oid)])
+ else:
+ algorithm = DerSequence([DerObjectId(key_oid), key_params])
+
+ pk_info = DerSequence([
+ 0,
+ algorithm,
+ DerOctetString(private_key)
+ ])
+ pk_info_der = pk_info.encode()
+
+ if passphrase is None:
+ return pk_info_der
+
+ if not passphrase:
+ raise ValueError("Empty passphrase")
+
+ # Encryption with PBES2
+ passphrase = tobytes(passphrase)
+ if protection is None:
+ protection = 'PBKDF2WithHMAC-SHA1AndDES-EDE3-CBC'
+ return PBES2.encrypt(pk_info_der, passphrase,
+ protection, prot_params, randfunc)
+
+
+def unwrap(p8_private_key, passphrase=None):
+ """Unwrap a private key from a PKCS#8 blob (clear or encrypted).
+
+ Args:
+ p8_private_key (bytes):
+ The private key wrapped into a PKCS#8 container, DER encoded.
+
+ Keyword Args:
+ passphrase (byte string or string):
+ The passphrase to use to decrypt the blob (if it is encrypted).
+
+ Return:
+ A tuple containing
+
+ #. the algorithm identifier of the wrapped key (OID, dotted string)
+ #. the private key (bytes, DER encoded)
+ #. the associated parameters (bytes, DER encoded) or ``None``
+
+ Raises:
+ ValueError : if decoding fails
+ """
+
+ if passphrase is not None:
+ passphrase = tobytes(passphrase)
+
+ found = False
+ try:
+ p8_private_key = PBES1.decrypt(p8_private_key, passphrase)
+ found = True
+ except PbesError as e:
+ error_str = "PBES1[%s]" % str(e)
+ except ValueError:
+ error_str = "PBES1[Invalid]"
+
+ if not found:
+ try:
+ p8_private_key = PBES2.decrypt(p8_private_key, passphrase)
+ found = True
+ except PbesError as e:
+ error_str += ",PBES2[%s]" % str(e)
+ except ValueError:
+ error_str += ",PBES2[Invalid]"
+
+ if not found:
+ raise ValueError("Error decoding PKCS#8 (%s)" % error_str)
+
+ pk_info = DerSequence().decode(p8_private_key, nr_elements=(2, 3, 4, 5))
+ if len(pk_info) == 2 and not passphrase:
+ raise ValueError("Not a valid clear PKCS#8 structure "
+ "(maybe it is encrypted?)")
+
+ # RFC5208, PKCS#8, version is v1(0)
+ #
+ # PrivateKeyInfo ::= SEQUENCE {
+ # version Version,
+ # privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
+ # privateKey PrivateKey,
+ # attributes [0] IMPLICIT Attributes OPTIONAL
+ # }
+ #
+ # RFC5915, Asymmetric Key Package, version is v2(1)
+ #
+ # OneAsymmetricKey ::= SEQUENCE {
+ # version Version,
+ # privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
+ # privateKey PrivateKey,
+ # attributes [0] Attributes OPTIONAL,
+ # ...,
+ # [[2: publicKey [1] PublicKey OPTIONAL ]],
+ # ...
+ # }
+
+ if pk_info[0] == 0:
+ if len(pk_info) not in (3, 4):
+ raise ValueError("Not a valid PrivateKeyInfo SEQUENCE")
+ elif pk_info[0] == 1:
+ if len(pk_info) not in (3, 4, 5):
+ raise ValueError("Not a valid PrivateKeyInfo SEQUENCE")
+ else:
+ raise ValueError("Not a valid PrivateKeyInfo SEQUENCE")
+
+ algo = DerSequence().decode(pk_info[1], nr_elements=(1, 2))
+ algo_oid = DerObjectId().decode(algo[0]).value
+ if len(algo) == 1:
+ algo_params = None
+ else:
+ try:
+ DerNull().decode(algo[1])
+ algo_params = None
+ except:
+ algo_params = algo[1]
+
+ # PrivateKey ::= OCTET STRING
+ private_key = DerOctetString().decode(pk_info[2]).payload
+
+ # We ignore attributes and (for v2 only) publickey
+
+ return (algo_oid, private_key, algo_params)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/IO/PKCS8.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/IO/PKCS8.pyi
new file mode 100644
index 00000000..cf6aa8ae
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/IO/PKCS8.pyi
@@ -0,0 +1,17 @@
+from typing import Tuple, Optional, Union, Callable
+from typing_extensions import NotRequired
+
+from Crypto.Util.asn1 import DerObject
+from Crypto.IO._PBES import ProtParams
+
+
+def wrap(private_key: bytes,
+ key_oid: str,
+ passphrase: Union[bytes, str] = ...,
+ protection: str = ...,
+ prot_params: Optional[ProtParams] = ...,
+ key_params: Optional[DerObject] = ...,
+ randfunc: Optional[Callable[[int], str]] = ...) -> bytes: ...
+
+
+def unwrap(p8_private_key: bytes, passphrase: Optional[Union[bytes, str]] = ...) -> Tuple[str, bytes, Optional[bytes]]: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/IO/_PBES.py b/backend/venv/lib/python3.9/site-packages/Crypto/IO/_PBES.py
new file mode 100644
index 00000000..93d39446
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/IO/_PBES.py
@@ -0,0 +1,546 @@
+#
+# PublicKey/_PBES.py : Password-Based Encryption functions
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 re
+
+from Crypto import Hash
+from Crypto import Random
+from Crypto.Util.asn1 import (
+ DerSequence, DerOctetString,
+ DerObjectId, DerInteger,
+ )
+
+from Crypto.Cipher import AES
+from Crypto.Util.Padding import pad, unpad
+from Crypto.Protocol.KDF import PBKDF1, PBKDF2, scrypt
+
+_OID_PBE_WITH_MD5_AND_DES_CBC = "1.2.840.113549.1.5.3"
+_OID_PBE_WITH_MD5_AND_RC2_CBC = "1.2.840.113549.1.5.6"
+_OID_PBE_WITH_SHA1_AND_DES_CBC = "1.2.840.113549.1.5.10"
+_OID_PBE_WITH_SHA1_AND_RC2_CBC = "1.2.840.113549.1.5.11"
+
+_OID_PBES2 = "1.2.840.113549.1.5.13"
+
+_OID_PBKDF2 = "1.2.840.113549.1.5.12"
+_OID_SCRYPT = "1.3.6.1.4.1.11591.4.11"
+
+_OID_HMAC_SHA1 = "1.2.840.113549.2.7"
+
+_OID_DES_EDE3_CBC = "1.2.840.113549.3.7"
+_OID_AES128_CBC = "2.16.840.1.101.3.4.1.2"
+_OID_AES192_CBC = "2.16.840.1.101.3.4.1.22"
+_OID_AES256_CBC = "2.16.840.1.101.3.4.1.42"
+_OID_AES128_GCM = "2.16.840.1.101.3.4.1.6"
+_OID_AES192_GCM = "2.16.840.1.101.3.4.1.26"
+_OID_AES256_GCM = "2.16.840.1.101.3.4.1.46"
+
+class PbesError(ValueError):
+ pass
+
+# These are the ASN.1 definitions used by the PBES1/2 logic:
+#
+# EncryptedPrivateKeyInfo ::= SEQUENCE {
+# encryptionAlgorithm EncryptionAlgorithmIdentifier,
+# encryptedData EncryptedData
+# }
+#
+# EncryptionAlgorithmIdentifier ::= AlgorithmIdentifier
+#
+# EncryptedData ::= OCTET STRING
+#
+# AlgorithmIdentifier ::= SEQUENCE {
+# algorithm OBJECT IDENTIFIER,
+# parameters ANY DEFINED BY algorithm OPTIONAL
+# }
+#
+# PBEParameter ::= SEQUENCE {
+# salt OCTET STRING (SIZE(8)),
+# iterationCount INTEGER
+# }
+#
+# PBES2-params ::= SEQUENCE {
+# keyDerivationFunc AlgorithmIdentifier {{PBES2-KDFs}},
+# encryptionScheme AlgorithmIdentifier {{PBES2-Encs}}
+# }
+#
+# PBKDF2-params ::= SEQUENCE {
+# salt CHOICE {
+# specified OCTET STRING,
+# otherSource AlgorithmIdentifier {{PBKDF2-SaltSources}}
+# },
+# iterationCount INTEGER (1..MAX),
+# keyLength INTEGER (1..MAX) OPTIONAL,
+# prf AlgorithmIdentifier {{PBKDF2-PRFs}} DEFAULT algid-hmacWithSHA1
+# }
+#
+# PBKDF2-PRFs ALGORITHM-IDENTIFIER ::= {
+# {NULL IDENTIFIED BY id-hmacWithSHA1},
+# {NULL IDENTIFIED BY id-hmacWithSHA224},
+# {NULL IDENTIFIED BY id-hmacWithSHA256},
+# {NULL IDENTIFIED BY id-hmacWithSHA384},
+# {NULL IDENTIFIED BY id-hmacWithSHA512},
+# {NULL IDENTIFIED BY id-hmacWithSHA512-224},
+# {NULL IDENTIFIED BY id-hmacWithSHA512-256},
+# ...
+# }
+# scrypt-params ::= SEQUENCE {
+# salt OCTET STRING,
+# costParameter INTEGER (1..MAX),
+# blockSize INTEGER (1..MAX),
+# parallelizationParameter INTEGER (1..MAX),
+# keyLength INTEGER (1..MAX) OPTIONAL
+# }
+
+
+class PBES1(object):
+ """Deprecated encryption scheme with password-based key derivation
+ (originally defined in PKCS#5 v1.5, but still present in `v2.0`__).
+
+ .. __: http://www.ietf.org/rfc/rfc2898.txt
+ """
+
+ @staticmethod
+ def decrypt(data, passphrase):
+ """Decrypt a piece of data using a passphrase and *PBES1*.
+
+ The algorithm to use is automatically detected.
+
+ :Parameters:
+ data : byte string
+ The piece of data to decrypt.
+ passphrase : byte string
+ The passphrase to use for decrypting the data.
+ :Returns:
+ The decrypted data, as a binary string.
+ """
+
+ enc_private_key_info = DerSequence().decode(data)
+ encrypted_algorithm = DerSequence().decode(enc_private_key_info[0])
+ encrypted_data = DerOctetString().decode(enc_private_key_info[1]).payload
+
+ pbe_oid = DerObjectId().decode(encrypted_algorithm[0]).value
+ cipher_params = {}
+ if pbe_oid == _OID_PBE_WITH_MD5_AND_DES_CBC:
+ # PBE_MD5_DES_CBC
+ from Crypto.Hash import MD5
+ from Crypto.Cipher import DES
+ hashmod = MD5
+ module = DES
+ elif pbe_oid == _OID_PBE_WITH_MD5_AND_RC2_CBC:
+ # PBE_MD5_RC2_CBC
+ from Crypto.Hash import MD5
+ from Crypto.Cipher import ARC2
+ hashmod = MD5
+ module = ARC2
+ cipher_params['effective_keylen'] = 64
+ elif pbe_oid == _OID_PBE_WITH_SHA1_AND_DES_CBC:
+ # PBE_SHA1_DES_CBC
+ from Crypto.Hash import SHA1
+ from Crypto.Cipher import DES
+ hashmod = SHA1
+ module = DES
+ elif pbe_oid == _OID_PBE_WITH_SHA1_AND_RC2_CBC:
+ # PBE_SHA1_RC2_CBC
+ from Crypto.Hash import SHA1
+ from Crypto.Cipher import ARC2
+ hashmod = SHA1
+ module = ARC2
+ cipher_params['effective_keylen'] = 64
+ else:
+ raise PbesError("Unknown OID for PBES1")
+
+ pbe_params = DerSequence().decode(encrypted_algorithm[1], nr_elements=2)
+ salt = DerOctetString().decode(pbe_params[0]).payload
+ iterations = pbe_params[1]
+
+ key_iv = PBKDF1(passphrase, salt, 16, iterations, hashmod)
+ key, iv = key_iv[:8], key_iv[8:]
+
+ cipher = module.new(key, module.MODE_CBC, iv, **cipher_params)
+ pt = cipher.decrypt(encrypted_data)
+ return unpad(pt, cipher.block_size)
+
+
+class PBES2(object):
+ """Encryption scheme with password-based key derivation
+ (defined in `PKCS#5 v2.0`__).
+
+ .. __: http://www.ietf.org/rfc/rfc2898.txt."""
+
+ @staticmethod
+ def encrypt(data, passphrase, protection, prot_params=None, randfunc=None):
+ """Encrypt a piece of data using a passphrase and *PBES2*.
+
+ :Parameters:
+ data : byte string
+ The piece of data to encrypt.
+ passphrase : byte string
+ The passphrase to use for encrypting the data.
+ protection : string
+ The identifier of the encryption algorithm to use.
+ The default value is '``PBKDF2WithHMAC-SHA1AndDES-EDE3-CBC``'.
+ prot_params : dictionary
+ Parameters of the protection algorithm.
+
+ +------------------+-----------------------------------------------+
+ | Key | Description |
+ +==================+===============================================+
+ | iteration_count | The KDF algorithm is repeated several times to|
+ | | slow down brute force attacks on passwords |
+ | | (called *N* or CPU/memory cost in scrypt). |
+ | | |
+ | | The default value for PBKDF2 is 1 000. |
+ | | The default value for scrypt is 16 384. |
+ +------------------+-----------------------------------------------+
+ | salt_size | Salt is used to thwart dictionary and rainbow |
+ | | attacks on passwords. The default value is 8 |
+ | | bytes. |
+ +------------------+-----------------------------------------------+
+ | block_size | *(scrypt only)* Memory-cost (r). The default |
+ | | value is 8. |
+ +------------------+-----------------------------------------------+
+ | parallelization | *(scrypt only)* CPU-cost (p). The default |
+ | | value is 1. |
+ +------------------+-----------------------------------------------+
+
+
+ randfunc : callable
+ Random number generation function; it should accept
+ a single integer N and return a string of random data,
+ N bytes long. If not specified, a new RNG will be
+ instantiated from ``Crypto.Random``.
+
+ :Returns:
+ The encrypted data, as a binary string.
+ """
+
+ if prot_params is None:
+ prot_params = {}
+
+ if randfunc is None:
+ randfunc = Random.new().read
+
+ pattern = re.compile(r'^(PBKDF2WithHMAC-([0-9A-Z-]+)|scrypt)And([0-9A-Z-]+)$')
+ res = pattern.match(protection)
+ if res is None:
+ raise ValueError("Unknown protection %s" % protection)
+
+ if protection.startswith("PBKDF"):
+ pbkdf = "pbkdf2"
+ pbkdf2_hmac_algo = res.group(2)
+ enc_algo = res.group(3)
+ else:
+ pbkdf = "scrypt"
+ enc_algo = res.group(3)
+
+ aead = False
+ if enc_algo == 'DES-EDE3-CBC':
+ from Crypto.Cipher import DES3
+ key_size = 24
+ module = DES3
+ cipher_mode = DES3.MODE_CBC
+ enc_oid = _OID_DES_EDE3_CBC
+ enc_param = {'iv': randfunc(8)}
+ elif enc_algo == 'AES128-CBC':
+ key_size = 16
+ module = AES
+ cipher_mode = AES.MODE_CBC
+ enc_oid = _OID_AES128_CBC
+ enc_param = {'iv': randfunc(16)}
+ elif enc_algo == 'AES192-CBC':
+ key_size = 24
+ module = AES
+ cipher_mode = AES.MODE_CBC
+ enc_oid = _OID_AES192_CBC
+ enc_param = {'iv': randfunc(16)}
+ elif enc_algo == 'AES256-CBC':
+ key_size = 32
+ module = AES
+ cipher_mode = AES.MODE_CBC
+ enc_oid = _OID_AES256_CBC
+ enc_param = {'iv': randfunc(16)}
+ elif enc_algo == 'AES128-GCM':
+ key_size = 16
+ module = AES
+ cipher_mode = AES.MODE_GCM
+ enc_oid = _OID_AES128_GCM
+ enc_param = {'nonce': randfunc(12)}
+ aead = True
+ elif enc_algo == 'AES192-GCM':
+ key_size = 24
+ module = AES
+ cipher_mode = AES.MODE_GCM
+ enc_oid = _OID_AES192_GCM
+ enc_param = {'nonce': randfunc(12)}
+ aead = True
+ elif enc_algo == 'AES256-GCM':
+ key_size = 32
+ module = AES
+ cipher_mode = AES.MODE_GCM
+ enc_oid = _OID_AES256_GCM
+ enc_param = {'nonce': randfunc(12)}
+ aead = True
+ else:
+ raise ValueError("Unknown encryption mode '%s'" % enc_algo)
+
+ iv_nonce = list(enc_param.values())[0]
+ salt = randfunc(prot_params.get("salt_size", 8))
+
+ # Derive key from password
+ if pbkdf == 'pbkdf2':
+
+ count = prot_params.get("iteration_count", 1000)
+ digestmod = Hash.new(pbkdf2_hmac_algo)
+
+ key = PBKDF2(passphrase,
+ salt,
+ key_size,
+ count,
+ hmac_hash_module=digestmod)
+
+ pbkdf2_params = DerSequence([
+ DerOctetString(salt),
+ DerInteger(count)
+ ])
+
+ if pbkdf2_hmac_algo != 'SHA1':
+ try:
+ hmac_oid = Hash.HMAC.new(b'', digestmod=digestmod).oid
+ except KeyError:
+ raise ValueError("No OID for HMAC hash algorithm")
+ pbkdf2_params.append(DerSequence([DerObjectId(hmac_oid)]))
+
+ kdf_info = DerSequence([
+ DerObjectId(_OID_PBKDF2), # PBKDF2
+ pbkdf2_params
+ ])
+
+ elif pbkdf == 'scrypt':
+
+ count = prot_params.get("iteration_count", 16384)
+ scrypt_r = prot_params.get('block_size', 8)
+ scrypt_p = prot_params.get('parallelization', 1)
+ key = scrypt(passphrase, salt, key_size,
+ count, scrypt_r, scrypt_p)
+ kdf_info = DerSequence([
+ DerObjectId(_OID_SCRYPT), # scrypt
+ DerSequence([
+ DerOctetString(salt),
+ DerInteger(count),
+ DerInteger(scrypt_r),
+ DerInteger(scrypt_p)
+ ])
+ ])
+
+ else:
+ raise ValueError("Unknown KDF " + res.group(1))
+
+ # Create cipher and use it
+ cipher = module.new(key, cipher_mode, **enc_param)
+ if aead:
+ ct, tag = cipher.encrypt_and_digest(data)
+ encrypted_data = ct + tag
+ else:
+ encrypted_data = cipher.encrypt(pad(data, cipher.block_size))
+ enc_info = DerSequence([
+ DerObjectId(enc_oid),
+ DerOctetString(iv_nonce)
+ ])
+
+ # Result
+ enc_private_key_info = DerSequence([
+ # encryptionAlgorithm
+ DerSequence([
+ DerObjectId(_OID_PBES2),
+ DerSequence([
+ kdf_info,
+ enc_info
+ ]),
+ ]),
+ DerOctetString(encrypted_data)
+ ])
+ return enc_private_key_info.encode()
+
+ @staticmethod
+ def decrypt(data, passphrase):
+ """Decrypt a piece of data using a passphrase and *PBES2*.
+
+ The algorithm to use is automatically detected.
+
+ :Parameters:
+ data : byte string
+ The piece of data to decrypt.
+ passphrase : byte string
+ The passphrase to use for decrypting the data.
+ :Returns:
+ The decrypted data, as a binary string.
+ """
+
+ enc_private_key_info = DerSequence().decode(data, nr_elements=2)
+ enc_algo = DerSequence().decode(enc_private_key_info[0])
+ encrypted_data = DerOctetString().decode(enc_private_key_info[1]).payload
+
+ pbe_oid = DerObjectId().decode(enc_algo[0]).value
+ if pbe_oid != _OID_PBES2:
+ raise PbesError("Not a PBES2 object")
+
+ pbes2_params = DerSequence().decode(enc_algo[1], nr_elements=2)
+
+ # Key Derivation Function selection
+ kdf_info = DerSequence().decode(pbes2_params[0], nr_elements=2)
+ kdf_oid = DerObjectId().decode(kdf_info[0]).value
+
+ kdf_key_length = None
+
+ # We only support PBKDF2 or scrypt
+ if kdf_oid == _OID_PBKDF2:
+
+ pbkdf2_params = DerSequence().decode(kdf_info[1], nr_elements=(2, 3, 4))
+ salt = DerOctetString().decode(pbkdf2_params[0]).payload
+ iteration_count = pbkdf2_params[1]
+
+ left = len(pbkdf2_params) - 2
+ idx = 2
+
+ if left > 0:
+ try:
+ # Check if it's an INTEGER
+ kdf_key_length = pbkdf2_params[idx] - 0
+ left -= 1
+ idx += 1
+ except TypeError:
+ # keyLength is not present
+ pass
+
+ # Default is HMAC-SHA1
+ pbkdf2_prf_oid = _OID_HMAC_SHA1
+ if left > 0:
+ pbkdf2_prf_algo_id = DerSequence().decode(pbkdf2_params[idx])
+ pbkdf2_prf_oid = DerObjectId().decode(pbkdf2_prf_algo_id[0]).value
+
+ elif kdf_oid == _OID_SCRYPT:
+
+ scrypt_params = DerSequence().decode(kdf_info[1], nr_elements=(4, 5))
+ salt = DerOctetString().decode(scrypt_params[0]).payload
+ iteration_count, scrypt_r, scrypt_p = [scrypt_params[x]
+ for x in (1, 2, 3)]
+ if len(scrypt_params) > 4:
+ kdf_key_length = scrypt_params[4]
+ else:
+ kdf_key_length = None
+ else:
+ raise PbesError("Unsupported PBES2 KDF")
+
+ # Cipher selection
+ enc_info = DerSequence().decode(pbes2_params[1])
+ enc_oid = DerObjectId().decode(enc_info[0]).value
+
+ aead = False
+ if enc_oid == _OID_DES_EDE3_CBC:
+ # DES_EDE3_CBC
+ from Crypto.Cipher import DES3
+ module = DES3
+ cipher_mode = DES3.MODE_CBC
+ key_size = 24
+ cipher_param = 'iv'
+ elif enc_oid == _OID_AES128_CBC:
+ module = AES
+ cipher_mode = AES.MODE_CBC
+ key_size = 16
+ cipher_param = 'iv'
+ elif enc_oid == _OID_AES192_CBC:
+ module = AES
+ cipher_mode = AES.MODE_CBC
+ key_size = 24
+ cipher_param = 'iv'
+ elif enc_oid == _OID_AES256_CBC:
+ module = AES
+ cipher_mode = AES.MODE_CBC
+ key_size = 32
+ cipher_param = 'iv'
+ elif enc_oid == _OID_AES128_GCM:
+ module = AES
+ cipher_mode = AES.MODE_GCM
+ key_size = 16
+ cipher_param = 'nonce'
+ aead = True
+ elif enc_oid == _OID_AES192_GCM:
+ module = AES
+ cipher_mode = AES.MODE_GCM
+ key_size = 24
+ cipher_param = 'nonce'
+ aead = True
+ elif enc_oid == _OID_AES256_GCM:
+ module = AES
+ cipher_mode = AES.MODE_GCM
+ key_size = 32
+ cipher_param = 'nonce'
+ aead = True
+ else:
+ raise PbesError("Unsupported PBES2 cipher " + enc_algo)
+
+ if kdf_key_length and kdf_key_length != key_size:
+ raise PbesError("Mismatch between PBES2 KDF parameters"
+ " and selected cipher")
+
+ iv_nonce = DerOctetString().decode(enc_info[1]).payload
+
+ # Create cipher
+ if kdf_oid == _OID_PBKDF2:
+
+ try:
+ hmac_hash_module_oid = Hash.HMAC._hmac2hash_oid[pbkdf2_prf_oid]
+ except KeyError:
+ raise PbesError("Unsupported HMAC %s" % pbkdf2_prf_oid)
+ hmac_hash_module = Hash.new(hmac_hash_module_oid)
+
+ key = PBKDF2(passphrase, salt, key_size, iteration_count,
+ hmac_hash_module=hmac_hash_module)
+ else:
+ key = scrypt(passphrase, salt, key_size, iteration_count,
+ scrypt_r, scrypt_p)
+ cipher = module.new(key, cipher_mode, **{cipher_param:iv_nonce})
+
+ # Decrypt data
+ if len(encrypted_data) < cipher.block_size:
+ raise ValueError("Too little data to decrypt")
+
+ if aead:
+ tag_len = cipher.block_size
+ pt = cipher.decrypt_and_verify(encrypted_data[:-tag_len],
+ encrypted_data[-tag_len:])
+ else:
+ pt_padded = cipher.decrypt(encrypted_data)
+ pt = unpad(pt_padded, cipher.block_size)
+
+ return pt
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/IO/_PBES.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/IO/_PBES.pyi
new file mode 100644
index 00000000..06733640
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/IO/_PBES.pyi
@@ -0,0 +1,26 @@
+from typing import Optional, Callable, TypedDict
+from typing_extensions import NotRequired
+
+class PbesError(ValueError):
+ ...
+
+class PBES1(object):
+ @staticmethod
+ def decrypt(data: bytes, passphrase: bytes) -> bytes: ...
+
+class ProtParams(TypedDict):
+ iteration_count: NotRequired[int]
+ salt_size: NotRequired[int]
+ block_size: NotRequired[int]
+ parallelization: NotRequired[int]
+
+class PBES2(object):
+ @staticmethod
+ def encrypt(data: bytes,
+ passphrase: bytes,
+ protection: str,
+ prot_params: Optional[ProtParams] = ...,
+ randfunc: Optional[Callable[[int],bytes]] = ...) -> bytes: ...
+
+ @staticmethod
+ def decrypt(data:bytes, passphrase: bytes) -> bytes: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/IO/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/IO/__init__.py
new file mode 100644
index 00000000..85a0d0b1
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/IO/__init__.py
@@ -0,0 +1,31 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+__all__ = ['PEM', 'PKCS8']
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/Numbers.py b/backend/venv/lib/python3.9/site-packages/Crypto/Math/Numbers.py
new file mode 100644
index 00000000..d9218f3d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/Numbers.py
@@ -0,0 +1,47 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+__all__ = ["Integer"]
+
+import os
+
+try:
+ if os.getenv("PYCRYPTODOME_DISABLE_GMP"):
+ raise ImportError()
+
+ from Crypto.Math._IntegerGMP import IntegerGMP as Integer
+ from Crypto.Math._IntegerGMP import implementation as _implementation
+except (ImportError, OSError, AttributeError):
+ try:
+ from Crypto.Math._IntegerCustom import IntegerCustom as Integer
+ from Crypto.Math._IntegerCustom import implementation as _implementation
+ except (ImportError, OSError):
+ from Crypto.Math._IntegerNative import IntegerNative as Integer
+ _implementation = {}
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/Numbers.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Math/Numbers.pyi
new file mode 100644
index 00000000..24e234b8
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/Numbers.pyi
@@ -0,0 +1,2 @@
+from Crypto.Math._IntegerBase import IntegerBase as Integer
+__all__ = ['Integer']
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/Primality.py b/backend/venv/lib/python3.9/site-packages/Crypto/Math/Primality.py
new file mode 100644
index 00000000..884c4185
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/Primality.py
@@ -0,0 +1,369 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""Functions to create and test prime numbers.
+
+:undocumented: __package__
+"""
+
+from Crypto import Random
+from Crypto.Math.Numbers import Integer
+
+from Crypto.Util.py3compat import iter_range
+
+COMPOSITE = 0
+PROBABLY_PRIME = 1
+
+
+def miller_rabin_test(candidate, iterations, randfunc=None):
+ """Perform a Miller-Rabin primality test on an integer.
+
+ The test is specified in Section C.3.1 of `FIPS PUB 186-4`__.
+
+ :Parameters:
+ candidate : integer
+ The number to test for primality.
+ iterations : integer
+ The maximum number of iterations to perform before
+ declaring a candidate a probable prime.
+ randfunc : callable
+ An RNG function where bases are taken from.
+
+ :Returns:
+ ``Primality.COMPOSITE`` or ``Primality.PROBABLY_PRIME``.
+
+ .. __: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
+ """
+
+ if not isinstance(candidate, Integer):
+ candidate = Integer(candidate)
+
+ if candidate in (1, 2, 3, 5):
+ return PROBABLY_PRIME
+
+ if candidate.is_even():
+ return COMPOSITE
+
+ one = Integer(1)
+ minus_one = Integer(candidate - 1)
+
+ if randfunc is None:
+ randfunc = Random.new().read
+
+ # Step 1 and 2
+ m = Integer(minus_one)
+ a = 0
+ while m.is_even():
+ m >>= 1
+ a += 1
+
+ # Skip step 3
+
+ # Step 4
+ for i in iter_range(iterations):
+
+ # Step 4.1-2
+ base = 1
+ while base in (one, minus_one):
+ base = Integer.random_range(min_inclusive=2,
+ max_inclusive=candidate - 2,
+ randfunc=randfunc)
+ assert(2 <= base <= candidate - 2)
+
+ # Step 4.3-4.4
+ z = pow(base, m, candidate)
+ if z in (one, minus_one):
+ continue
+
+ # Step 4.5
+ for j in iter_range(1, a):
+ z = pow(z, 2, candidate)
+ if z == minus_one:
+ break
+ if z == one:
+ return COMPOSITE
+ else:
+ return COMPOSITE
+
+ # Step 5
+ return PROBABLY_PRIME
+
+
+def lucas_test(candidate):
+ """Perform a Lucas primality test on an integer.
+
+ The test is specified in Section C.3.3 of `FIPS PUB 186-4`__.
+
+ :Parameters:
+ candidate : integer
+ The number to test for primality.
+
+ :Returns:
+ ``Primality.COMPOSITE`` or ``Primality.PROBABLY_PRIME``.
+
+ .. __: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
+ """
+
+ if not isinstance(candidate, Integer):
+ candidate = Integer(candidate)
+
+ # Step 1
+ if candidate in (1, 2, 3, 5):
+ return PROBABLY_PRIME
+ if candidate.is_even() or candidate.is_perfect_square():
+ return COMPOSITE
+
+ # Step 2
+ def alternate():
+ value = 5
+ while True:
+ yield value
+ if value > 0:
+ value += 2
+ else:
+ value -= 2
+ value = -value
+
+ for D in alternate():
+ if candidate in (D, -D):
+ continue
+ js = Integer.jacobi_symbol(D, candidate)
+ if js == 0:
+ return COMPOSITE
+ if js == -1:
+ break
+ # Found D. P=1 and Q=(1-D)/4 (note that Q is guaranteed to be an integer)
+
+ # Step 3
+ # This is \delta(n) = n - jacobi(D/n)
+ K = candidate + 1
+ # Step 4
+ r = K.size_in_bits() - 1
+ # Step 5
+ # U_1=1 and V_1=P
+ U_i = Integer(1)
+ V_i = Integer(1)
+ U_temp = Integer(0)
+ V_temp = Integer(0)
+ # Step 6
+ for i in iter_range(r - 1, -1, -1):
+ # Square
+ # U_temp = U_i * V_i % candidate
+ U_temp.set(U_i)
+ U_temp *= V_i
+ U_temp %= candidate
+ # V_temp = (((V_i ** 2 + (U_i ** 2 * D)) * K) >> 1) % candidate
+ V_temp.set(U_i)
+ V_temp *= U_i
+ V_temp *= D
+ V_temp.multiply_accumulate(V_i, V_i)
+ if V_temp.is_odd():
+ V_temp += candidate
+ V_temp >>= 1
+ V_temp %= candidate
+ # Multiply
+ if K.get_bit(i):
+ # U_i = (((U_temp + V_temp) * K) >> 1) % candidate
+ U_i.set(U_temp)
+ U_i += V_temp
+ if U_i.is_odd():
+ U_i += candidate
+ U_i >>= 1
+ U_i %= candidate
+ # V_i = (((V_temp + U_temp * D) * K) >> 1) % candidate
+ V_i.set(V_temp)
+ V_i.multiply_accumulate(U_temp, D)
+ if V_i.is_odd():
+ V_i += candidate
+ V_i >>= 1
+ V_i %= candidate
+ else:
+ U_i.set(U_temp)
+ V_i.set(V_temp)
+ # Step 7
+ if U_i == 0:
+ return PROBABLY_PRIME
+ return COMPOSITE
+
+
+from Crypto.Util.number import sieve_base as _sieve_base_large
+## The optimal number of small primes to use for the sieve
+## is probably dependent on the platform and the candidate size
+_sieve_base = set(_sieve_base_large[:100])
+
+
+def test_probable_prime(candidate, randfunc=None):
+ """Test if a number is prime.
+
+ A number is qualified as prime if it passes a certain
+ number of Miller-Rabin tests (dependent on the size
+ of the number, but such that probability of a false
+ positive is less than 10^-30) and a single Lucas test.
+
+ For instance, a 1024-bit candidate will need to pass
+ 4 Miller-Rabin tests.
+
+ :Parameters:
+ candidate : integer
+ The number to test for primality.
+ randfunc : callable
+ The routine to draw random bytes from to select Miller-Rabin bases.
+ :Returns:
+ ``PROBABLE_PRIME`` if the number if prime with very high probability.
+ ``COMPOSITE`` if the number is a composite.
+ For efficiency reasons, ``COMPOSITE`` is also returned for small primes.
+ """
+
+ if randfunc is None:
+ randfunc = Random.new().read
+
+ if not isinstance(candidate, Integer):
+ candidate = Integer(candidate)
+
+ # First, check trial division by the smallest primes
+ if int(candidate) in _sieve_base:
+ return PROBABLY_PRIME
+ try:
+ map(candidate.fail_if_divisible_by, _sieve_base)
+ except ValueError:
+ return COMPOSITE
+
+ # These are the number of Miller-Rabin iterations s.t. p(k, t) < 1E-30,
+ # with p(k, t) being the probability that a randomly chosen k-bit number
+ # is composite but still survives t MR iterations.
+ mr_ranges = ((220, 30), (280, 20), (390, 15), (512, 10),
+ (620, 7), (740, 6), (890, 5), (1200, 4),
+ (1700, 3), (3700, 2))
+
+ bit_size = candidate.size_in_bits()
+ try:
+ mr_iterations = list(filter(lambda x: bit_size < x[0],
+ mr_ranges))[0][1]
+ except IndexError:
+ mr_iterations = 1
+
+ if miller_rabin_test(candidate, mr_iterations,
+ randfunc=randfunc) == COMPOSITE:
+ return COMPOSITE
+ if lucas_test(candidate) == COMPOSITE:
+ return COMPOSITE
+ return PROBABLY_PRIME
+
+
+def generate_probable_prime(**kwargs):
+ """Generate a random probable prime.
+
+ The prime will not have any specific properties
+ (e.g. it will not be a *strong* prime).
+
+ Random numbers are evaluated for primality until one
+ passes all tests, consisting of a certain number of
+ Miller-Rabin tests with random bases followed by
+ a single Lucas test.
+
+ The number of Miller-Rabin iterations is chosen such that
+ the probability that the output number is a non-prime is
+ less than 1E-30 (roughly 2^{-100}).
+
+ This approach is compliant to `FIPS PUB 186-4`__.
+
+ :Keywords:
+ exact_bits : integer
+ The desired size in bits of the probable prime.
+ It must be at least 160.
+ randfunc : callable
+ An RNG function where candidate primes are taken from.
+ prime_filter : callable
+ A function that takes an Integer as parameter and returns
+ True if the number can be passed to further primality tests,
+ False if it should be immediately discarded.
+
+ :Return:
+ A probable prime in the range 2^exact_bits > p > 2^(exact_bits-1).
+
+ .. __: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
+ """
+
+ exact_bits = kwargs.pop("exact_bits", None)
+ randfunc = kwargs.pop("randfunc", None)
+ prime_filter = kwargs.pop("prime_filter", lambda x: True)
+ if kwargs:
+ raise ValueError("Unknown parameters: " + kwargs.keys())
+
+ if exact_bits is None:
+ raise ValueError("Missing exact_bits parameter")
+ if exact_bits < 160:
+ raise ValueError("Prime number is not big enough.")
+
+ if randfunc is None:
+ randfunc = Random.new().read
+
+ result = COMPOSITE
+ while result == COMPOSITE:
+ candidate = Integer.random(exact_bits=exact_bits,
+ randfunc=randfunc) | 1
+ if not prime_filter(candidate):
+ continue
+ result = test_probable_prime(candidate, randfunc)
+ return candidate
+
+
+def generate_probable_safe_prime(**kwargs):
+ """Generate a random, probable safe prime.
+
+ Note this operation is much slower than generating a simple prime.
+
+ :Keywords:
+ exact_bits : integer
+ The desired size in bits of the probable safe prime.
+ randfunc : callable
+ An RNG function where candidate primes are taken from.
+
+ :Return:
+ A probable safe prime in the range
+ 2^exact_bits > p > 2^(exact_bits-1).
+ """
+
+ exact_bits = kwargs.pop("exact_bits", None)
+ randfunc = kwargs.pop("randfunc", None)
+ if kwargs:
+ raise ValueError("Unknown parameters: " + kwargs.keys())
+
+ if randfunc is None:
+ randfunc = Random.new().read
+
+ result = COMPOSITE
+ while result == COMPOSITE:
+ q = generate_probable_prime(exact_bits=exact_bits - 1, randfunc=randfunc)
+ candidate = q * 2 + 1
+ if candidate.size_in_bits() != exact_bits:
+ continue
+ result = test_probable_prime(candidate, randfunc=randfunc)
+ return candidate
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/Primality.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Math/Primality.pyi
new file mode 100644
index 00000000..78134830
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/Primality.pyi
@@ -0,0 +1,18 @@
+from typing import Callable, Optional, Union, Set
+
+PrimeResult = int
+
+COMPOSITE: PrimeResult
+PROBABLY_PRIME: PrimeResult
+
+def miller_rabin_test(candidate: int, iterations: int, randfunc: Optional[Callable[[int],bytes]]=None) -> PrimeResult: ...
+def lucas_test(candidate: int) -> PrimeResult: ...
+_sieve_base: Set[int]
+def test_probable_prime(candidate: int, randfunc: Optional[Callable[[int],bytes]]=None) -> PrimeResult: ...
+def generate_probable_prime(*,
+ exact_bits: int = ...,
+ randfunc: Callable[[int],bytes] = ...,
+ prime_filter: Callable[[int],bool] = ...) -> int: ...
+def generate_probable_safe_prime(*,
+ exact_bits: int = ...,
+ randfunc: Callable[[int],bytes] = ...) -> int: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerBase.py b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerBase.py
new file mode 100644
index 00000000..931743aa
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerBase.py
@@ -0,0 +1,412 @@
+# ===================================================================
+#
+# Copyright (c) 2018, Helder Eijs
+# 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 abc
+
+from Crypto.Util.py3compat import iter_range, bord, bchr, ABC
+
+from Crypto import Random
+
+
+class IntegerBase(ABC):
+
+ # Conversions
+ @abc.abstractmethod
+ def __int__(self):
+ pass
+
+ @abc.abstractmethod
+ def __str__(self):
+ pass
+
+ @abc.abstractmethod
+ def __repr__(self):
+ pass
+
+ @abc.abstractmethod
+ def to_bytes(self, block_size=0, byteorder='big'):
+ pass
+
+ @staticmethod
+ @abc.abstractmethod
+ def from_bytes(byte_string, byteorder='big'):
+ pass
+
+ # Relations
+ @abc.abstractmethod
+ def __eq__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __ne__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __lt__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __le__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __gt__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __ge__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __nonzero__(self):
+ pass
+ __bool__ = __nonzero__
+
+ @abc.abstractmethod
+ def is_negative(self):
+ pass
+
+ # Arithmetic operations
+ @abc.abstractmethod
+ def __add__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __sub__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __mul__(self, factor):
+ pass
+
+ @abc.abstractmethod
+ def __floordiv__(self, divisor):
+ pass
+
+ @abc.abstractmethod
+ def __mod__(self, divisor):
+ pass
+
+ @abc.abstractmethod
+ def inplace_pow(self, exponent, modulus=None):
+ pass
+
+ @abc.abstractmethod
+ def __pow__(self, exponent, modulus=None):
+ pass
+
+ @abc.abstractmethod
+ def __abs__(self):
+ pass
+
+ @abc.abstractmethod
+ def sqrt(self, modulus=None):
+ pass
+
+ @abc.abstractmethod
+ def __iadd__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __isub__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __imul__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __imod__(self, term):
+ pass
+
+ # Boolean/bit operations
+ @abc.abstractmethod
+ def __and__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __or__(self, term):
+ pass
+
+ @abc.abstractmethod
+ def __rshift__(self, pos):
+ pass
+
+ @abc.abstractmethod
+ def __irshift__(self, pos):
+ pass
+
+ @abc.abstractmethod
+ def __lshift__(self, pos):
+ pass
+
+ @abc.abstractmethod
+ def __ilshift__(self, pos):
+ pass
+
+ @abc.abstractmethod
+ def get_bit(self, n):
+ pass
+
+ # Extra
+ @abc.abstractmethod
+ def is_odd(self):
+ pass
+
+ @abc.abstractmethod
+ def is_even(self):
+ pass
+
+ @abc.abstractmethod
+ def size_in_bits(self):
+ pass
+
+ @abc.abstractmethod
+ def size_in_bytes(self):
+ pass
+
+ @abc.abstractmethod
+ def is_perfect_square(self):
+ pass
+
+ @abc.abstractmethod
+ def fail_if_divisible_by(self, small_prime):
+ pass
+
+ @abc.abstractmethod
+ def multiply_accumulate(self, a, b):
+ pass
+
+ @abc.abstractmethod
+ def set(self, source):
+ pass
+
+ @abc.abstractmethod
+ def inplace_inverse(self, modulus):
+ pass
+
+ @abc.abstractmethod
+ def inverse(self, modulus):
+ pass
+
+ @abc.abstractmethod
+ def gcd(self, term):
+ pass
+
+ @abc.abstractmethod
+ def lcm(self, term):
+ pass
+
+ @staticmethod
+ @abc.abstractmethod
+ def jacobi_symbol(a, n):
+ pass
+
+ @staticmethod
+ def _tonelli_shanks(n, p):
+ """Tonelli-shanks algorithm for computing the square root
+ of n modulo a prime p.
+
+ n must be in the range [0..p-1].
+ p must be at least even.
+
+ The return value r is the square root of modulo p. If non-zero,
+ another solution will also exist (p-r).
+
+ Note we cannot assume that p is really a prime: if it's not,
+ we can either raise an exception or return the correct value.
+ """
+
+ # See https://rosettacode.org/wiki/Tonelli-Shanks_algorithm
+
+ if n in (0, 1):
+ return n
+
+ if p % 4 == 3:
+ root = pow(n, (p + 1) // 4, p)
+ if pow(root, 2, p) != n:
+ raise ValueError("Cannot compute square root")
+ return root
+
+ s = 1
+ q = (p - 1) // 2
+ while not (q & 1):
+ s += 1
+ q >>= 1
+
+ z = n.__class__(2)
+ while True:
+ euler = pow(z, (p - 1) // 2, p)
+ if euler == 1:
+ z += 1
+ continue
+ if euler == p - 1:
+ break
+ # Most probably p is not a prime
+ raise ValueError("Cannot compute square root")
+
+ m = s
+ c = pow(z, q, p)
+ t = pow(n, q, p)
+ r = pow(n, (q + 1) // 2, p)
+
+ while t != 1:
+ for i in iter_range(0, m):
+ if pow(t, 2**i, p) == 1:
+ break
+ if i == m:
+ raise ValueError("Cannot compute square root of %d mod %d" % (n, p))
+ b = pow(c, 2**(m - i - 1), p)
+ m = i
+ c = b**2 % p
+ t = (t * b**2) % p
+ r = (r * b) % p
+
+ if pow(r, 2, p) != n:
+ raise ValueError("Cannot compute square root")
+
+ return r
+
+ @classmethod
+ def random(cls, **kwargs):
+ """Generate a random natural integer of a certain size.
+
+ :Keywords:
+ exact_bits : positive integer
+ The length in bits of the resulting random Integer number.
+ The number is guaranteed to fulfil the relation:
+
+ 2^bits > result >= 2^(bits - 1)
+
+ max_bits : positive integer
+ The maximum length in bits of the resulting random Integer number.
+ The number is guaranteed to fulfil the relation:
+
+ 2^bits > result >=0
+
+ randfunc : callable
+ A function that returns a random byte string. The length of the
+ byte string is passed as parameter. Optional.
+ If not provided (or ``None``), randomness is read from the system RNG.
+
+ :Return: a Integer object
+ """
+
+ exact_bits = kwargs.pop("exact_bits", None)
+ max_bits = kwargs.pop("max_bits", None)
+ randfunc = kwargs.pop("randfunc", None)
+
+ if randfunc is None:
+ randfunc = Random.new().read
+
+ if exact_bits is None and max_bits is None:
+ raise ValueError("Either 'exact_bits' or 'max_bits' must be specified")
+
+ if exact_bits is not None and max_bits is not None:
+ raise ValueError("'exact_bits' and 'max_bits' are mutually exclusive")
+
+ bits = exact_bits or max_bits
+ bytes_needed = ((bits - 1) // 8) + 1
+ significant_bits_msb = 8 - (bytes_needed * 8 - bits)
+ msb = bord(randfunc(1)[0])
+ if exact_bits is not None:
+ msb |= 1 << (significant_bits_msb - 1)
+ msb &= (1 << significant_bits_msb) - 1
+
+ return cls.from_bytes(bchr(msb) + randfunc(bytes_needed - 1))
+
+ @classmethod
+ def random_range(cls, **kwargs):
+ """Generate a random integer within a given internal.
+
+ :Keywords:
+ min_inclusive : integer
+ The lower end of the interval (inclusive).
+ max_inclusive : integer
+ The higher end of the interval (inclusive).
+ max_exclusive : integer
+ The higher end of the interval (exclusive).
+ randfunc : callable
+ A function that returns a random byte string. The length of the
+ byte string is passed as parameter. Optional.
+ If not provided (or ``None``), randomness is read from the system RNG.
+ :Returns:
+ An Integer randomly taken in the given interval.
+ """
+
+ min_inclusive = kwargs.pop("min_inclusive", None)
+ max_inclusive = kwargs.pop("max_inclusive", None)
+ max_exclusive = kwargs.pop("max_exclusive", None)
+ randfunc = kwargs.pop("randfunc", None)
+
+ if kwargs:
+ raise ValueError("Unknown keywords: " + str(kwargs.keys))
+ if None not in (max_inclusive, max_exclusive):
+ raise ValueError("max_inclusive and max_exclusive cannot be both"
+ " specified")
+ if max_exclusive is not None:
+ max_inclusive = max_exclusive - 1
+ if None in (min_inclusive, max_inclusive):
+ raise ValueError("Missing keyword to identify the interval")
+
+ if randfunc is None:
+ randfunc = Random.new().read
+
+ norm_maximum = max_inclusive - min_inclusive
+ bits_needed = cls(norm_maximum).size_in_bits()
+
+ norm_candidate = -1
+ while not 0 <= norm_candidate <= norm_maximum:
+ norm_candidate = cls.random(
+ max_bits=bits_needed,
+ randfunc=randfunc
+ )
+ return norm_candidate + min_inclusive
+
+ @staticmethod
+ @abc.abstractmethod
+ def _mult_modulo_bytes(term1, term2, modulus):
+ """Multiply two integers, take the modulo, and encode as big endian.
+ This specialized method is used for RSA decryption.
+
+ Args:
+ term1 : integer
+ The first term of the multiplication, non-negative.
+ term2 : integer
+ The second term of the multiplication, non-negative.
+ modulus: integer
+ The modulus, a positive odd number.
+ :Returns:
+ A byte string, with the result of the modular multiplication
+ encoded in big endian mode.
+ It is as long as the modulus would be, with zero padding
+ on the left if needed.
+ """
+ pass
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerBase.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerBase.pyi
new file mode 100644
index 00000000..ea235326
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerBase.pyi
@@ -0,0 +1,67 @@
+from typing import Optional, Union, Callable
+
+RandFunc = Callable[[int],int]
+
+class IntegerBase:
+
+ def __init__(self, value: Union[IntegerBase, int]): ...
+
+ def __int__(self) -> int: ...
+ def __str__(self) -> str: ...
+ def __repr__(self) -> str: ...
+ def to_bytes(self, block_size: Optional[int]=0, byteorder: str= ...) -> bytes: ...
+ @staticmethod
+ def from_bytes(byte_string: bytes, byteorder: Optional[str] = ...) -> IntegerBase: ...
+ def __eq__(self, term: object) -> bool: ...
+ def __ne__(self, term: object) -> bool: ...
+ def __lt__(self, term: Union[IntegerBase, int]) -> bool: ...
+ def __le__(self, term: Union[IntegerBase, int]) -> bool: ...
+ def __gt__(self, term: Union[IntegerBase, int]) -> bool: ...
+ def __ge__(self, term: Union[IntegerBase, int]) -> bool: ...
+ def __nonzero__(self) -> bool: ...
+ def is_negative(self) -> bool: ...
+ def __add__(self, term: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __sub__(self, term: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __mul__(self, term: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __floordiv__(self, divisor: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __mod__(self, divisor: Union[IntegerBase, int]) -> IntegerBase: ...
+ def inplace_pow(self, exponent: int, modulus: Optional[Union[IntegerBase, int]]=None) -> IntegerBase: ...
+ def __pow__(self, exponent: int, modulus: Optional[int]) -> IntegerBase: ...
+ def __abs__(self) -> IntegerBase: ...
+ def sqrt(self, modulus: Optional[int]) -> IntegerBase: ...
+ def __iadd__(self, term: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __isub__(self, term: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __imul__(self, term: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __imod__(self, divisor: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __and__(self, term: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __or__(self, term: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __rshift__(self, pos: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __irshift__(self, pos: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __lshift__(self, pos: Union[IntegerBase, int]) -> IntegerBase: ...
+ def __ilshift__(self, pos: Union[IntegerBase, int]) -> IntegerBase: ...
+ def get_bit(self, n: int) -> bool: ...
+ def is_odd(self) -> bool: ...
+ def is_even(self) -> bool: ...
+ def size_in_bits(self) -> int: ...
+ def size_in_bytes(self) -> int: ...
+ def is_perfect_square(self) -> bool: ...
+ def fail_if_divisible_by(self, small_prime: Union[IntegerBase, int]) -> None: ...
+ def multiply_accumulate(self, a: Union[IntegerBase, int], b: Union[IntegerBase, int]) -> IntegerBase: ...
+ def set(self, source: Union[IntegerBase, int]) -> IntegerBase: ...
+ def inplace_inverse(self, modulus: Union[IntegerBase, int]) -> IntegerBase: ...
+ def inverse(self, modulus: Union[IntegerBase, int]) -> IntegerBase: ...
+ def gcd(self, term: Union[IntegerBase, int]) -> IntegerBase: ...
+ def lcm(self, term: Union[IntegerBase, int]) -> IntegerBase: ...
+ @staticmethod
+ def jacobi_symbol(a: Union[IntegerBase, int], n: Union[IntegerBase, int]) -> IntegerBase: ...
+ @staticmethod
+ def _tonelli_shanks(n: Union[IntegerBase, int], p: Union[IntegerBase, int]) -> IntegerBase : ...
+ @classmethod
+ def random(cls, **kwargs: Union[int,RandFunc]) -> IntegerBase : ...
+ @classmethod
+ def random_range(cls, **kwargs: Union[int,RandFunc]) -> IntegerBase : ...
+ @staticmethod
+ def _mult_modulo_bytes(term1: Union[IntegerBase, int],
+ term2: Union[IntegerBase, int],
+ modulus: Union[IntegerBase, int]) -> bytes: ...
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerCustom.py b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerCustom.py
new file mode 100644
index 00000000..7dfc2350
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerCustom.py
@@ -0,0 +1,162 @@
+# ===================================================================
+#
+# Copyright (c) 2018, Helder Eijs
+# 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 ._IntegerNative import IntegerNative
+
+from Crypto.Util.number import long_to_bytes, bytes_to_long
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ create_string_buffer,
+ get_raw_buffer, backend,
+ c_size_t, c_ulonglong)
+
+
+from Crypto.Random.random import getrandbits
+
+c_defs = """
+int monty_pow(uint8_t *out,
+ const uint8_t *base,
+ const uint8_t *exp,
+ const uint8_t *modulus,
+ size_t len,
+ uint64_t seed);
+
+int monty_multiply(uint8_t *out,
+ const uint8_t *term1,
+ const uint8_t *term2,
+ const uint8_t *modulus,
+ size_t len);
+"""
+
+
+_raw_montgomery = load_pycryptodome_raw_lib("Crypto.Math._modexp", c_defs)
+implementation = {"library": "custom", "api": backend}
+
+
+class IntegerCustom(IntegerNative):
+
+ @staticmethod
+ def from_bytes(byte_string, byteorder='big'):
+ if byteorder == 'big':
+ pass
+ elif byteorder == 'little':
+ byte_string = bytearray(byte_string)
+ byte_string.reverse()
+ else:
+ raise ValueError("Incorrect byteorder")
+ return IntegerCustom(bytes_to_long(byte_string))
+
+ def inplace_pow(self, exponent, modulus=None):
+ exp_value = int(exponent)
+ if exp_value < 0:
+ raise ValueError("Exponent must not be negative")
+
+ # No modular reduction
+ if modulus is None:
+ self._value = pow(self._value, exp_value)
+ return self
+
+ # With modular reduction
+ mod_value = int(modulus)
+ if mod_value < 0:
+ raise ValueError("Modulus must be positive")
+ if mod_value == 0:
+ raise ZeroDivisionError("Modulus cannot be zero")
+
+ # C extension only works with odd moduli
+ if (mod_value & 1) == 0:
+ self._value = pow(self._value, exp_value, mod_value)
+ return self
+
+ # C extension only works with bases smaller than modulus
+ if self._value >= mod_value:
+ self._value %= mod_value
+
+ max_len = len(long_to_bytes(max(self._value, exp_value, mod_value)))
+
+ base_b = long_to_bytes(self._value, max_len)
+ exp_b = long_to_bytes(exp_value, max_len)
+ modulus_b = long_to_bytes(mod_value, max_len)
+
+ out = create_string_buffer(max_len)
+
+ error = _raw_montgomery.monty_pow(
+ out,
+ base_b,
+ exp_b,
+ modulus_b,
+ c_size_t(max_len),
+ c_ulonglong(getrandbits(64))
+ )
+
+ if error:
+ raise ValueError("monty_pow failed with error: %d" % error)
+
+ result = bytes_to_long(get_raw_buffer(out))
+ self._value = result
+ return self
+
+ @staticmethod
+ def _mult_modulo_bytes(term1, term2, modulus):
+
+ # With modular reduction
+ mod_value = int(modulus)
+ if mod_value < 0:
+ raise ValueError("Modulus must be positive")
+ if mod_value == 0:
+ raise ZeroDivisionError("Modulus cannot be zero")
+
+ # C extension only works with odd moduli
+ if (mod_value & 1) == 0:
+ raise ValueError("Odd modulus is required")
+
+ # C extension only works with non-negative terms smaller than modulus
+ if term1 >= mod_value or term1 < 0:
+ term1 %= mod_value
+ if term2 >= mod_value or term2 < 0:
+ term2 %= mod_value
+
+ modulus_b = long_to_bytes(mod_value)
+ numbers_len = len(modulus_b)
+ term1_b = long_to_bytes(term1, numbers_len)
+ term2_b = long_to_bytes(term2, numbers_len)
+ out = create_string_buffer(numbers_len)
+
+ error = _raw_montgomery.monty_multiply(
+ out,
+ term1_b,
+ term2_b,
+ modulus_b,
+ c_size_t(numbers_len)
+ )
+ if error:
+ raise ValueError("monty_multiply failed with error: %d" % error)
+
+ return get_raw_buffer(out)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerCustom.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerCustom.pyi
new file mode 100644
index 00000000..2dd75c74
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerCustom.pyi
@@ -0,0 +1,8 @@
+from typing import Any
+
+from ._IntegerNative import IntegerNative
+
+_raw_montgomery = Any
+
+class IntegerCustom(IntegerNative):
+ pass
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerGMP.py b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerGMP.py
new file mode 100644
index 00000000..46118ae0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerGMP.py
@@ -0,0 +1,799 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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
+import struct
+
+from Crypto.Util.py3compat import is_native_int
+
+from Crypto.Util._raw_api import (backend, load_lib,
+ c_ulong, c_size_t, c_uint8_ptr)
+
+from ._IntegerBase import IntegerBase
+
+gmp_defs = """typedef unsigned long UNIX_ULONG;
+ typedef struct { int a; int b; void *c; } MPZ;
+ typedef MPZ mpz_t[1];
+ typedef UNIX_ULONG mp_bitcnt_t;
+
+ void __gmpz_init (mpz_t x);
+ void __gmpz_init_set (mpz_t rop, const mpz_t op);
+ void __gmpz_init_set_ui (mpz_t rop, UNIX_ULONG op);
+
+ UNIX_ULONG __gmpz_get_ui (const mpz_t op);
+ void __gmpz_set (mpz_t rop, const mpz_t op);
+ void __gmpz_set_ui (mpz_t rop, UNIX_ULONG op);
+ void __gmpz_add (mpz_t rop, const mpz_t op1, const mpz_t op2);
+ void __gmpz_add_ui (mpz_t rop, const mpz_t op1, UNIX_ULONG op2);
+ void __gmpz_sub_ui (mpz_t rop, const mpz_t op1, UNIX_ULONG op2);
+ void __gmpz_addmul (mpz_t rop, const mpz_t op1, const mpz_t op2);
+ void __gmpz_addmul_ui (mpz_t rop, const mpz_t op1, UNIX_ULONG op2);
+ void __gmpz_submul_ui (mpz_t rop, const mpz_t op1, UNIX_ULONG op2);
+ void __gmpz_import (mpz_t rop, size_t count, int order, size_t size,
+ int endian, size_t nails, const void *op);
+ void * __gmpz_export (void *rop, size_t *countp, int order,
+ size_t size,
+ int endian, size_t nails, const mpz_t op);
+ size_t __gmpz_sizeinbase (const mpz_t op, int base);
+ void __gmpz_sub (mpz_t rop, const mpz_t op1, const mpz_t op2);
+ void __gmpz_mul (mpz_t rop, const mpz_t op1, const mpz_t op2);
+ void __gmpz_mul_ui (mpz_t rop, const mpz_t op1, UNIX_ULONG op2);
+ int __gmpz_cmp (const mpz_t op1, const mpz_t op2);
+ void __gmpz_powm (mpz_t rop, const mpz_t base, const mpz_t exp, const
+ mpz_t mod);
+ void __gmpz_powm_ui (mpz_t rop, const mpz_t base, UNIX_ULONG exp,
+ const mpz_t mod);
+ void __gmpz_pow_ui (mpz_t rop, const mpz_t base, UNIX_ULONG exp);
+ void __gmpz_sqrt(mpz_t rop, const mpz_t op);
+ void __gmpz_mod (mpz_t r, const mpz_t n, const mpz_t d);
+ void __gmpz_neg (mpz_t rop, const mpz_t op);
+ void __gmpz_abs (mpz_t rop, const mpz_t op);
+ void __gmpz_and (mpz_t rop, const mpz_t op1, const mpz_t op2);
+ void __gmpz_ior (mpz_t rop, const mpz_t op1, const mpz_t op2);
+ void __gmpz_clear (mpz_t x);
+ void __gmpz_tdiv_q_2exp (mpz_t q, const mpz_t n, mp_bitcnt_t b);
+ void __gmpz_fdiv_q (mpz_t q, const mpz_t n, const mpz_t d);
+ void __gmpz_mul_2exp (mpz_t rop, const mpz_t op1, mp_bitcnt_t op2);
+ int __gmpz_tstbit (const mpz_t op, mp_bitcnt_t bit_index);
+ int __gmpz_perfect_square_p (const mpz_t op);
+ int __gmpz_jacobi (const mpz_t a, const mpz_t b);
+ void __gmpz_gcd (mpz_t rop, const mpz_t op1, const mpz_t op2);
+ UNIX_ULONG __gmpz_gcd_ui (mpz_t rop, const mpz_t op1,
+ UNIX_ULONG op2);
+ void __gmpz_lcm (mpz_t rop, const mpz_t op1, const mpz_t op2);
+ int __gmpz_invert (mpz_t rop, const mpz_t op1, const mpz_t op2);
+ int __gmpz_divisible_p (const mpz_t n, const mpz_t d);
+ int __gmpz_divisible_ui_p (const mpz_t n, UNIX_ULONG d);
+
+ size_t __gmpz_size (const mpz_t op);
+ UNIX_ULONG __gmpz_getlimbn (const mpz_t op, size_t n);
+ """
+
+if sys.platform == "win32":
+ raise ImportError("Not using GMP on Windows")
+
+lib = load_lib("gmp", gmp_defs)
+implementation = {"library": "gmp", "api": backend}
+
+if hasattr(lib, "__mpir_version"):
+ raise ImportError("MPIR library detected")
+
+
+# Lazy creation of GMP methods
+class _GMP(object):
+
+ def __getattr__(self, name):
+ if name.startswith("mpz_"):
+ func_name = "__gmpz_" + name[4:]
+ elif name.startswith("gmp_"):
+ func_name = "__gmp_" + name[4:]
+ else:
+ raise AttributeError("Attribute %s is invalid" % name)
+ func = getattr(lib, func_name)
+ setattr(self, name, func)
+ return func
+
+
+_gmp = _GMP()
+
+
+# In order to create a function that returns a pointer to
+# a new MPZ structure, we need to break the abstraction
+# and know exactly what ffi backend we have
+if implementation["api"] == "ctypes":
+ from ctypes import Structure, c_int, c_void_p, byref
+
+ class _MPZ(Structure):
+ _fields_ = [('_mp_alloc', c_int),
+ ('_mp_size', c_int),
+ ('_mp_d', c_void_p)]
+
+ def new_mpz():
+ return byref(_MPZ())
+
+ _gmp.mpz_getlimbn.restype = c_ulong
+
+else:
+ # We are using CFFI
+ from Crypto.Util._raw_api import ffi
+
+ def new_mpz():
+ return ffi.new("MPZ*")
+
+
+# Size of a native word
+_sys_bits = 8 * struct.calcsize("P")
+
+
+class IntegerGMP(IntegerBase):
+ """A fast, arbitrary precision integer"""
+
+ _zero_mpz_p = new_mpz()
+ _gmp.mpz_init_set_ui(_zero_mpz_p, c_ulong(0))
+
+ def __init__(self, value):
+ """Initialize the integer to the given value."""
+
+ self._mpz_p = new_mpz()
+ self._initialized = False
+
+ if isinstance(value, float):
+ raise ValueError("A floating point type is not a natural number")
+
+ if is_native_int(value):
+ _gmp.mpz_init(self._mpz_p)
+ self._initialized = True
+ if value == 0:
+ return
+
+ tmp = new_mpz()
+ _gmp.mpz_init(tmp)
+
+ try:
+ positive = value >= 0
+ reduce = abs(value)
+ slots = (reduce.bit_length() - 1) // 32 + 1
+
+ while slots > 0:
+ slots = slots - 1
+ _gmp.mpz_set_ui(tmp,
+ c_ulong(0xFFFFFFFF & (reduce >> (slots * 32))))
+ _gmp.mpz_mul_2exp(tmp, tmp, c_ulong(slots * 32))
+ _gmp.mpz_add(self._mpz_p, self._mpz_p, tmp)
+ finally:
+ _gmp.mpz_clear(tmp)
+
+ if not positive:
+ _gmp.mpz_neg(self._mpz_p, self._mpz_p)
+
+ elif isinstance(value, IntegerGMP):
+ _gmp.mpz_init_set(self._mpz_p, value._mpz_p)
+ self._initialized = True
+ else:
+ raise NotImplementedError
+
+ # Conversions
+ def __int__(self):
+ tmp = new_mpz()
+ _gmp.mpz_init_set(tmp, self._mpz_p)
+
+ try:
+ value = 0
+ slot = 0
+ while _gmp.mpz_cmp(tmp, self._zero_mpz_p) != 0:
+ lsb = _gmp.mpz_get_ui(tmp) & 0xFFFFFFFF
+ value |= lsb << (slot * 32)
+ _gmp.mpz_tdiv_q_2exp(tmp, tmp, c_ulong(32))
+ slot = slot + 1
+ finally:
+ _gmp.mpz_clear(tmp)
+
+ if self < 0:
+ value = -value
+ return int(value)
+
+ def __str__(self):
+ return str(int(self))
+
+ def __repr__(self):
+ return "Integer(%s)" % str(self)
+
+ # Only Python 2.x
+ def __hex__(self):
+ return hex(int(self))
+
+ # Only Python 3.x
+ def __index__(self):
+ return int(self)
+
+ def to_bytes(self, block_size=0, byteorder='big'):
+ """Convert the number into a byte string.
+
+ This method encodes the number in network order and prepends
+ as many zero bytes as required. It only works for non-negative
+ values.
+
+ :Parameters:
+ block_size : integer
+ The exact size the output byte string must have.
+ If zero, the string has the minimal length.
+ byteorder : string
+ 'big' for big-endian integers (default), 'little' for litte-endian.
+ :Returns:
+ A byte string.
+ :Raise ValueError:
+ If the value is negative or if ``block_size`` is
+ provided and the length of the byte string would exceed it.
+ """
+
+ if self < 0:
+ raise ValueError("Conversion only valid for non-negative numbers")
+
+ num_limbs = _gmp.mpz_size(self._mpz_p)
+ if _sys_bits == 32:
+ spchar = "L"
+ num_limbs = max(1, num_limbs, (block_size + 3) // 4)
+ elif _sys_bits == 64:
+ spchar = "Q"
+ num_limbs = max(1, num_limbs, (block_size + 7) // 8)
+ else:
+ raise ValueError("Unknown limb size")
+
+ # mpz_getlimbn returns 0 if i is larger than the number of actual limbs
+ limbs = [_gmp.mpz_getlimbn(self._mpz_p, num_limbs - i - 1) for i in range(num_limbs)]
+
+ result = struct.pack(">" + spchar * num_limbs, *limbs)
+ cutoff_len = len(result) - block_size
+ if block_size == 0:
+ result = result.lstrip(b'\x00')
+ elif cutoff_len > 0:
+ if result[:cutoff_len] != b'\x00' * (cutoff_len):
+ raise ValueError("Number is too big to convert to "
+ "byte string of prescribed length")
+ result = result[cutoff_len:]
+ elif cutoff_len < 0:
+ result = b'\x00' * (-cutoff_len) + result
+
+ if byteorder == 'little':
+ result = result[::-1]
+ elif byteorder == 'big':
+ pass
+ else:
+ raise ValueError("Incorrect byteorder")
+
+ if len(result) == 0:
+ result = b'\x00'
+
+ return result
+
+ @staticmethod
+ def from_bytes(byte_string, byteorder='big'):
+ """Convert a byte string into a number.
+
+ :Parameters:
+ byte_string : byte string
+ The input number, encoded in network order.
+ It can only be non-negative.
+ byteorder : string
+ 'big' for big-endian integers (default), 'little' for litte-endian.
+
+ :Return:
+ The ``Integer`` object carrying the same value as the input.
+ """
+ result = IntegerGMP(0)
+ if byteorder == 'big':
+ pass
+ elif byteorder == 'little':
+ byte_string = bytearray(byte_string)
+ byte_string.reverse()
+ else:
+ raise ValueError("Incorrect byteorder")
+ _gmp.mpz_import(
+ result._mpz_p,
+ c_size_t(len(byte_string)), # Amount of words to read
+ 1, # Big endian
+ c_size_t(1), # Each word is 1 byte long
+ 0, # Endianess within a word - not relevant
+ c_size_t(0), # No nails
+ c_uint8_ptr(byte_string))
+ return result
+
+ # Relations
+ def _apply_and_return(self, func, term):
+ if not isinstance(term, IntegerGMP):
+ term = IntegerGMP(term)
+ return func(self._mpz_p, term._mpz_p)
+
+ def __eq__(self, term):
+ if not (isinstance(term, IntegerGMP) or is_native_int(term)):
+ return False
+ return self._apply_and_return(_gmp.mpz_cmp, term) == 0
+
+ def __ne__(self, term):
+ if not (isinstance(term, IntegerGMP) or is_native_int(term)):
+ return True
+ return self._apply_and_return(_gmp.mpz_cmp, term) != 0
+
+ def __lt__(self, term):
+ return self._apply_and_return(_gmp.mpz_cmp, term) < 0
+
+ def __le__(self, term):
+ return self._apply_and_return(_gmp.mpz_cmp, term) <= 0
+
+ def __gt__(self, term):
+ return self._apply_and_return(_gmp.mpz_cmp, term) > 0
+
+ def __ge__(self, term):
+ return self._apply_and_return(_gmp.mpz_cmp, term) >= 0
+
+ def __nonzero__(self):
+ return _gmp.mpz_cmp(self._mpz_p, self._zero_mpz_p) != 0
+ __bool__ = __nonzero__
+
+ def is_negative(self):
+ return _gmp.mpz_cmp(self._mpz_p, self._zero_mpz_p) < 0
+
+ # Arithmetic operations
+ def __add__(self, term):
+ result = IntegerGMP(0)
+ if not isinstance(term, IntegerGMP):
+ try:
+ term = IntegerGMP(term)
+ except NotImplementedError:
+ return NotImplemented
+ _gmp.mpz_add(result._mpz_p,
+ self._mpz_p,
+ term._mpz_p)
+ return result
+
+ def __sub__(self, term):
+ result = IntegerGMP(0)
+ if not isinstance(term, IntegerGMP):
+ try:
+ term = IntegerGMP(term)
+ except NotImplementedError:
+ return NotImplemented
+ _gmp.mpz_sub(result._mpz_p,
+ self._mpz_p,
+ term._mpz_p)
+ return result
+
+ def __mul__(self, term):
+ result = IntegerGMP(0)
+ if not isinstance(term, IntegerGMP):
+ try:
+ term = IntegerGMP(term)
+ except NotImplementedError:
+ return NotImplemented
+ _gmp.mpz_mul(result._mpz_p,
+ self._mpz_p,
+ term._mpz_p)
+ return result
+
+ def __floordiv__(self, divisor):
+ if not isinstance(divisor, IntegerGMP):
+ divisor = IntegerGMP(divisor)
+ if _gmp.mpz_cmp(divisor._mpz_p,
+ self._zero_mpz_p) == 0:
+ raise ZeroDivisionError("Division by zero")
+ result = IntegerGMP(0)
+ _gmp.mpz_fdiv_q(result._mpz_p,
+ self._mpz_p,
+ divisor._mpz_p)
+ return result
+
+ def __mod__(self, divisor):
+ if not isinstance(divisor, IntegerGMP):
+ divisor = IntegerGMP(divisor)
+ comp = _gmp.mpz_cmp(divisor._mpz_p,
+ self._zero_mpz_p)
+ if comp == 0:
+ raise ZeroDivisionError("Division by zero")
+ if comp < 0:
+ raise ValueError("Modulus must be positive")
+ result = IntegerGMP(0)
+ _gmp.mpz_mod(result._mpz_p,
+ self._mpz_p,
+ divisor._mpz_p)
+ return result
+
+ def inplace_pow(self, exponent, modulus=None):
+
+ if modulus is None:
+ if exponent < 0:
+ raise ValueError("Exponent must not be negative")
+
+ # Normal exponentiation
+ if exponent > 256:
+ raise ValueError("Exponent is too big")
+ _gmp.mpz_pow_ui(self._mpz_p,
+ self._mpz_p, # Base
+ c_ulong(int(exponent))
+ )
+ else:
+ # Modular exponentiation
+ if not isinstance(modulus, IntegerGMP):
+ modulus = IntegerGMP(modulus)
+ if not modulus:
+ raise ZeroDivisionError("Division by zero")
+ if modulus.is_negative():
+ raise ValueError("Modulus must be positive")
+ if is_native_int(exponent):
+ if exponent < 0:
+ raise ValueError("Exponent must not be negative")
+ if exponent < 65536:
+ _gmp.mpz_powm_ui(self._mpz_p,
+ self._mpz_p,
+ c_ulong(exponent),
+ modulus._mpz_p)
+ return self
+ exponent = IntegerGMP(exponent)
+ elif exponent.is_negative():
+ raise ValueError("Exponent must not be negative")
+ _gmp.mpz_powm(self._mpz_p,
+ self._mpz_p,
+ exponent._mpz_p,
+ modulus._mpz_p)
+ return self
+
+ def __pow__(self, exponent, modulus=None):
+ result = IntegerGMP(self)
+ return result.inplace_pow(exponent, modulus)
+
+ def __abs__(self):
+ result = IntegerGMP(0)
+ _gmp.mpz_abs(result._mpz_p, self._mpz_p)
+ return result
+
+ def sqrt(self, modulus=None):
+ """Return the largest Integer that does not
+ exceed the square root"""
+
+ if modulus is None:
+ if self < 0:
+ raise ValueError("Square root of negative value")
+ result = IntegerGMP(0)
+ _gmp.mpz_sqrt(result._mpz_p,
+ self._mpz_p)
+ else:
+ if modulus <= 0:
+ raise ValueError("Modulus must be positive")
+ modulus = int(modulus)
+ result = IntegerGMP(self._tonelli_shanks(int(self) % modulus, modulus))
+
+ return result
+
+ def __iadd__(self, term):
+ if is_native_int(term):
+ if 0 <= term < 65536:
+ _gmp.mpz_add_ui(self._mpz_p,
+ self._mpz_p,
+ c_ulong(term))
+ return self
+ if -65535 < term < 0:
+ _gmp.mpz_sub_ui(self._mpz_p,
+ self._mpz_p,
+ c_ulong(-term))
+ return self
+ term = IntegerGMP(term)
+ _gmp.mpz_add(self._mpz_p,
+ self._mpz_p,
+ term._mpz_p)
+ return self
+
+ def __isub__(self, term):
+ if is_native_int(term):
+ if 0 <= term < 65536:
+ _gmp.mpz_sub_ui(self._mpz_p,
+ self._mpz_p,
+ c_ulong(term))
+ return self
+ if -65535 < term < 0:
+ _gmp.mpz_add_ui(self._mpz_p,
+ self._mpz_p,
+ c_ulong(-term))
+ return self
+ term = IntegerGMP(term)
+ _gmp.mpz_sub(self._mpz_p,
+ self._mpz_p,
+ term._mpz_p)
+ return self
+
+ def __imul__(self, term):
+ if is_native_int(term):
+ if 0 <= term < 65536:
+ _gmp.mpz_mul_ui(self._mpz_p,
+ self._mpz_p,
+ c_ulong(term))
+ return self
+ if -65535 < term < 0:
+ _gmp.mpz_mul_ui(self._mpz_p,
+ self._mpz_p,
+ c_ulong(-term))
+ _gmp.mpz_neg(self._mpz_p, self._mpz_p)
+ return self
+ term = IntegerGMP(term)
+ _gmp.mpz_mul(self._mpz_p,
+ self._mpz_p,
+ term._mpz_p)
+ return self
+
+ def __imod__(self, divisor):
+ if not isinstance(divisor, IntegerGMP):
+ divisor = IntegerGMP(divisor)
+ comp = _gmp.mpz_cmp(divisor._mpz_p,
+ divisor._zero_mpz_p)
+ if comp == 0:
+ raise ZeroDivisionError("Division by zero")
+ if comp < 0:
+ raise ValueError("Modulus must be positive")
+ _gmp.mpz_mod(self._mpz_p,
+ self._mpz_p,
+ divisor._mpz_p)
+ return self
+
+ # Boolean/bit operations
+ def __and__(self, term):
+ result = IntegerGMP(0)
+ if not isinstance(term, IntegerGMP):
+ term = IntegerGMP(term)
+ _gmp.mpz_and(result._mpz_p,
+ self._mpz_p,
+ term._mpz_p)
+ return result
+
+ def __or__(self, term):
+ result = IntegerGMP(0)
+ if not isinstance(term, IntegerGMP):
+ term = IntegerGMP(term)
+ _gmp.mpz_ior(result._mpz_p,
+ self._mpz_p,
+ term._mpz_p)
+ return result
+
+ def __rshift__(self, pos):
+ result = IntegerGMP(0)
+ if pos < 0:
+ raise ValueError("negative shift count")
+ if pos > 65536:
+ if self < 0:
+ return -1
+ else:
+ return 0
+ _gmp.mpz_tdiv_q_2exp(result._mpz_p,
+ self._mpz_p,
+ c_ulong(int(pos)))
+ return result
+
+ def __irshift__(self, pos):
+ if pos < 0:
+ raise ValueError("negative shift count")
+ if pos > 65536:
+ if self < 0:
+ return -1
+ else:
+ return 0
+ _gmp.mpz_tdiv_q_2exp(self._mpz_p,
+ self._mpz_p,
+ c_ulong(int(pos)))
+ return self
+
+ def __lshift__(self, pos):
+ result = IntegerGMP(0)
+ if not 0 <= pos < 65536:
+ raise ValueError("Incorrect shift count")
+ _gmp.mpz_mul_2exp(result._mpz_p,
+ self._mpz_p,
+ c_ulong(int(pos)))
+ return result
+
+ def __ilshift__(self, pos):
+ if not 0 <= pos < 65536:
+ raise ValueError("Incorrect shift count")
+ _gmp.mpz_mul_2exp(self._mpz_p,
+ self._mpz_p,
+ c_ulong(int(pos)))
+ return self
+
+ def get_bit(self, n):
+ """Return True if the n-th bit is set to 1.
+ Bit 0 is the least significant."""
+
+ if self < 0:
+ raise ValueError("no bit representation for negative values")
+ if n < 0:
+ raise ValueError("negative bit count")
+ if n > 65536:
+ return 0
+ return bool(_gmp.mpz_tstbit(self._mpz_p,
+ c_ulong(int(n))))
+
+ # Extra
+ def is_odd(self):
+ return _gmp.mpz_tstbit(self._mpz_p, 0) == 1
+
+ def is_even(self):
+ return _gmp.mpz_tstbit(self._mpz_p, 0) == 0
+
+ def size_in_bits(self):
+ """Return the minimum number of bits that can encode the number."""
+
+ if self < 0:
+ raise ValueError("Conversion only valid for non-negative numbers")
+ return _gmp.mpz_sizeinbase(self._mpz_p, 2)
+
+ def size_in_bytes(self):
+ """Return the minimum number of bytes that can encode the number."""
+ return (self.size_in_bits() - 1) // 8 + 1
+
+ def is_perfect_square(self):
+ return _gmp.mpz_perfect_square_p(self._mpz_p) != 0
+
+ def fail_if_divisible_by(self, small_prime):
+ """Raise an exception if the small prime is a divisor."""
+
+ if is_native_int(small_prime):
+ if 0 < small_prime < 65536:
+ if _gmp.mpz_divisible_ui_p(self._mpz_p,
+ c_ulong(small_prime)):
+ raise ValueError("The value is composite")
+ return
+ small_prime = IntegerGMP(small_prime)
+ if _gmp.mpz_divisible_p(self._mpz_p,
+ small_prime._mpz_p):
+ raise ValueError("The value is composite")
+
+ def multiply_accumulate(self, a, b):
+ """Increment the number by the product of a and b."""
+
+ if not isinstance(a, IntegerGMP):
+ a = IntegerGMP(a)
+ if is_native_int(b):
+ if 0 < b < 65536:
+ _gmp.mpz_addmul_ui(self._mpz_p,
+ a._mpz_p,
+ c_ulong(b))
+ return self
+ if -65535 < b < 0:
+ _gmp.mpz_submul_ui(self._mpz_p,
+ a._mpz_p,
+ c_ulong(-b))
+ return self
+ b = IntegerGMP(b)
+ _gmp.mpz_addmul(self._mpz_p,
+ a._mpz_p,
+ b._mpz_p)
+ return self
+
+ def set(self, source):
+ """Set the Integer to have the given value"""
+
+ if not isinstance(source, IntegerGMP):
+ source = IntegerGMP(source)
+ _gmp.mpz_set(self._mpz_p,
+ source._mpz_p)
+ return self
+
+ def inplace_inverse(self, modulus):
+ """Compute the inverse of this number in the ring of
+ modulo integers.
+
+ Raise an exception if no inverse exists.
+ """
+
+ if not isinstance(modulus, IntegerGMP):
+ modulus = IntegerGMP(modulus)
+
+ comp = _gmp.mpz_cmp(modulus._mpz_p,
+ self._zero_mpz_p)
+ if comp == 0:
+ raise ZeroDivisionError("Modulus cannot be zero")
+ if comp < 0:
+ raise ValueError("Modulus must be positive")
+
+ result = _gmp.mpz_invert(self._mpz_p,
+ self._mpz_p,
+ modulus._mpz_p)
+ if not result:
+ raise ValueError("No inverse value can be computed")
+ return self
+
+ def inverse(self, modulus):
+ result = IntegerGMP(self)
+ result.inplace_inverse(modulus)
+ return result
+
+ def gcd(self, term):
+ """Compute the greatest common denominator between this
+ number and another term."""
+
+ result = IntegerGMP(0)
+ if is_native_int(term):
+ if 0 < term < 65535:
+ _gmp.mpz_gcd_ui(result._mpz_p,
+ self._mpz_p,
+ c_ulong(term))
+ return result
+ term = IntegerGMP(term)
+ _gmp.mpz_gcd(result._mpz_p, self._mpz_p, term._mpz_p)
+ return result
+
+ def lcm(self, term):
+ """Compute the least common multiplier between this
+ number and another term."""
+
+ result = IntegerGMP(0)
+ if not isinstance(term, IntegerGMP):
+ term = IntegerGMP(term)
+ _gmp.mpz_lcm(result._mpz_p, self._mpz_p, term._mpz_p)
+ return result
+
+ @staticmethod
+ def jacobi_symbol(a, n):
+ """Compute the Jacobi symbol"""
+
+ if not isinstance(a, IntegerGMP):
+ a = IntegerGMP(a)
+ if not isinstance(n, IntegerGMP):
+ n = IntegerGMP(n)
+ if n <= 0 or n.is_even():
+ raise ValueError("n must be positive odd for the Jacobi symbol")
+ return _gmp.mpz_jacobi(a._mpz_p, n._mpz_p)
+
+ @staticmethod
+ def _mult_modulo_bytes(term1, term2, modulus):
+ if not isinstance(term1, IntegerGMP):
+ term1 = IntegerGMP(term1)
+ if not isinstance(term2, IntegerGMP):
+ term2 = IntegerGMP(term2)
+ if not isinstance(modulus, IntegerGMP):
+ modulus = IntegerGMP(modulus)
+
+ if modulus < 0:
+ raise ValueError("Modulus must be positive")
+ if modulus == 0:
+ raise ZeroDivisionError("Modulus cannot be zero")
+ if (modulus & 1) == 0:
+ raise ValueError("Odd modulus is required")
+
+ product = (term1 * term2) % modulus
+ return product.to_bytes(modulus.size_in_bytes())
+
+ # Clean-up
+ def __del__(self):
+
+ try:
+ if self._mpz_p is not None:
+ if self._initialized:
+ _gmp.mpz_clear(self._mpz_p)
+
+ self._mpz_p = None
+ except AttributeError:
+ pass
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerGMP.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerGMP.pyi
new file mode 100644
index 00000000..2181b470
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerGMP.pyi
@@ -0,0 +1,3 @@
+from ._IntegerBase import IntegerBase
+class IntegerGMP(IntegerBase):
+ pass
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerNative.py b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerNative.py
new file mode 100644
index 00000000..e9937d09
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerNative.py
@@ -0,0 +1,382 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 ._IntegerBase import IntegerBase
+
+from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse, GCD
+
+
+class IntegerNative(IntegerBase):
+ """A class to model a natural integer (including zero)"""
+
+ def __init__(self, value):
+ if isinstance(value, float):
+ raise ValueError("A floating point type is not a natural number")
+ try:
+ self._value = value._value
+ except AttributeError:
+ self._value = value
+
+ # Conversions
+ def __int__(self):
+ return self._value
+
+ def __str__(self):
+ return str(int(self))
+
+ def __repr__(self):
+ return "Integer(%s)" % str(self)
+
+ # Only Python 2.x
+ def __hex__(self):
+ return hex(self._value)
+
+ # Only Python 3.x
+ def __index__(self):
+ return int(self._value)
+
+ def to_bytes(self, block_size=0, byteorder='big'):
+ if self._value < 0:
+ raise ValueError("Conversion only valid for non-negative numbers")
+ result = long_to_bytes(self._value, block_size)
+ if len(result) > block_size > 0:
+ raise ValueError("Value too large to encode")
+ if byteorder == 'big':
+ pass
+ elif byteorder == 'little':
+ result = bytearray(result)
+ result.reverse()
+ result = bytes(result)
+ else:
+ raise ValueError("Incorrect byteorder")
+ return result
+
+ @classmethod
+ def from_bytes(cls, byte_string, byteorder='big'):
+ if byteorder == 'big':
+ pass
+ elif byteorder == 'little':
+ byte_string = bytearray(byte_string)
+ byte_string.reverse()
+ else:
+ raise ValueError("Incorrect byteorder")
+ return cls(bytes_to_long(byte_string))
+
+ # Relations
+ def __eq__(self, term):
+ if term is None:
+ return False
+ return self._value == int(term)
+
+ def __ne__(self, term):
+ return not self.__eq__(term)
+
+ def __lt__(self, term):
+ return self._value < int(term)
+
+ def __le__(self, term):
+ return self.__lt__(term) or self.__eq__(term)
+
+ def __gt__(self, term):
+ return not self.__le__(term)
+
+ def __ge__(self, term):
+ return not self.__lt__(term)
+
+ def __nonzero__(self):
+ return self._value != 0
+ __bool__ = __nonzero__
+
+ def is_negative(self):
+ return self._value < 0
+
+ # Arithmetic operations
+ def __add__(self, term):
+ try:
+ return self.__class__(self._value + int(term))
+ except (ValueError, AttributeError, TypeError):
+ return NotImplemented
+
+ def __sub__(self, term):
+ try:
+ return self.__class__(self._value - int(term))
+ except (ValueError, AttributeError, TypeError):
+ return NotImplemented
+
+ def __mul__(self, factor):
+ try:
+ return self.__class__(self._value * int(factor))
+ except (ValueError, AttributeError, TypeError):
+ return NotImplemented
+
+ def __floordiv__(self, divisor):
+ return self.__class__(self._value // int(divisor))
+
+ def __mod__(self, divisor):
+ divisor_value = int(divisor)
+ if divisor_value < 0:
+ raise ValueError("Modulus must be positive")
+ return self.__class__(self._value % divisor_value)
+
+ def inplace_pow(self, exponent, modulus=None):
+ exp_value = int(exponent)
+ if exp_value < 0:
+ raise ValueError("Exponent must not be negative")
+
+ if modulus is not None:
+ mod_value = int(modulus)
+ if mod_value < 0:
+ raise ValueError("Modulus must be positive")
+ if mod_value == 0:
+ raise ZeroDivisionError("Modulus cannot be zero")
+ else:
+ mod_value = None
+ self._value = pow(self._value, exp_value, mod_value)
+ return self
+
+ def __pow__(self, exponent, modulus=None):
+ result = self.__class__(self)
+ return result.inplace_pow(exponent, modulus)
+
+ def __abs__(self):
+ return abs(self._value)
+
+ def sqrt(self, modulus=None):
+
+ value = self._value
+ if modulus is None:
+ if value < 0:
+ raise ValueError("Square root of negative value")
+ # http://stackoverflow.com/questions/15390807/integer-square-root-in-python
+
+ x = value
+ y = (x + 1) // 2
+ while y < x:
+ x = y
+ y = (x + value // x) // 2
+ result = x
+ else:
+ if modulus <= 0:
+ raise ValueError("Modulus must be positive")
+ result = self._tonelli_shanks(self % modulus, modulus)
+
+ return self.__class__(result)
+
+ def __iadd__(self, term):
+ self._value += int(term)
+ return self
+
+ def __isub__(self, term):
+ self._value -= int(term)
+ return self
+
+ def __imul__(self, term):
+ self._value *= int(term)
+ return self
+
+ def __imod__(self, term):
+ modulus = int(term)
+ if modulus == 0:
+ raise ZeroDivisionError("Division by zero")
+ if modulus < 0:
+ raise ValueError("Modulus must be positive")
+ self._value %= modulus
+ return self
+
+ # Boolean/bit operations
+ def __and__(self, term):
+ return self.__class__(self._value & int(term))
+
+ def __or__(self, term):
+ return self.__class__(self._value | int(term))
+
+ def __rshift__(self, pos):
+ try:
+ return self.__class__(self._value >> int(pos))
+ except OverflowError:
+ if self._value >= 0:
+ return 0
+ else:
+ return -1
+
+ def __irshift__(self, pos):
+ try:
+ self._value >>= int(pos)
+ except OverflowError:
+ if self._value >= 0:
+ return 0
+ else:
+ return -1
+ return self
+
+ def __lshift__(self, pos):
+ try:
+ return self.__class__(self._value << int(pos))
+ except OverflowError:
+ raise ValueError("Incorrect shift count")
+
+ def __ilshift__(self, pos):
+ try:
+ self._value <<= int(pos)
+ except OverflowError:
+ raise ValueError("Incorrect shift count")
+ return self
+
+ def get_bit(self, n):
+ if self._value < 0:
+ raise ValueError("no bit representation for negative values")
+ try:
+ try:
+ result = (self._value >> n._value) & 1
+ if n._value < 0:
+ raise ValueError("negative bit count")
+ except AttributeError:
+ result = (self._value >> n) & 1
+ if n < 0:
+ raise ValueError("negative bit count")
+ except OverflowError:
+ result = 0
+ return result
+
+ # Extra
+ def is_odd(self):
+ return (self._value & 1) == 1
+
+ def is_even(self):
+ return (self._value & 1) == 0
+
+ def size_in_bits(self):
+
+ if self._value < 0:
+ raise ValueError("Conversion only valid for non-negative numbers")
+
+ if self._value == 0:
+ return 1
+
+ return self._value.bit_length()
+
+ def size_in_bytes(self):
+ return (self.size_in_bits() - 1) // 8 + 1
+
+ def is_perfect_square(self):
+ if self._value < 0:
+ return False
+ if self._value in (0, 1):
+ return True
+
+ x = self._value // 2
+ square_x = x ** 2
+
+ while square_x > self._value:
+ x = (square_x + self._value) // (2 * x)
+ square_x = x ** 2
+
+ return self._value == x ** 2
+
+ def fail_if_divisible_by(self, small_prime):
+ if (self._value % int(small_prime)) == 0:
+ raise ValueError("Value is composite")
+
+ def multiply_accumulate(self, a, b):
+ self._value += int(a) * int(b)
+ return self
+
+ def set(self, source):
+ self._value = int(source)
+
+ def inplace_inverse(self, modulus):
+ self._value = inverse(self._value, int(modulus))
+ return self
+
+ def inverse(self, modulus):
+ result = self.__class__(self)
+ result.inplace_inverse(modulus)
+ return result
+
+ def gcd(self, term):
+ return self.__class__(GCD(abs(self._value), abs(int(term))))
+
+ def lcm(self, term):
+ term = int(term)
+ if self._value == 0 or term == 0:
+ return self.__class__(0)
+ return self.__class__(abs((self._value * term) // self.gcd(term)._value))
+
+ @staticmethod
+ def jacobi_symbol(a, n):
+ a = int(a)
+ n = int(n)
+
+ if n <= 0:
+ raise ValueError("n must be a positive integer")
+
+ if (n & 1) == 0:
+ raise ValueError("n must be odd for the Jacobi symbol")
+
+ # Step 1
+ a = a % n
+ # Step 2
+ if a == 1 or n == 1:
+ return 1
+ # Step 3
+ if a == 0:
+ return 0
+ # Step 4
+ e = 0
+ a1 = a
+ while (a1 & 1) == 0:
+ a1 >>= 1
+ e += 1
+ # Step 5
+ if (e & 1) == 0:
+ s = 1
+ elif n % 8 in (1, 7):
+ s = 1
+ else:
+ s = -1
+ # Step 6
+ if n % 4 == 3 and a1 % 4 == 3:
+ s = -s
+ # Step 7
+ n1 = n % a1
+ # Step 8
+ return s * IntegerNative.jacobi_symbol(n1, a1)
+
+ @staticmethod
+ def _mult_modulo_bytes(term1, term2, modulus):
+ if modulus < 0:
+ raise ValueError("Modulus must be positive")
+ if modulus == 0:
+ raise ZeroDivisionError("Modulus cannot be zero")
+ if (modulus & 1) == 0:
+ raise ValueError("Odd modulus is required")
+
+ number_len = len(long_to_bytes(modulus))
+ return long_to_bytes((term1 * term2) % modulus, number_len)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerNative.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerNative.pyi
new file mode 100644
index 00000000..3f65a39e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_IntegerNative.pyi
@@ -0,0 +1,3 @@
+from ._IntegerBase import IntegerBase
+class IntegerNative(IntegerBase):
+ pass
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/Math/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Math/_modexp.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_modexp.abi3.so
new file mode 100755
index 00000000..ed632c3a
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Math/_modexp.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/DH.py b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/DH.py
new file mode 100644
index 00000000..f70db9d9
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/DH.py
@@ -0,0 +1,192 @@
+from Crypto.Util.number import long_to_bytes
+from Crypto.PublicKey.ECC import (EccKey,
+ construct,
+ _import_curve25519_public_key,
+ _import_curve448_public_key)
+
+
+def _compute_ecdh(key_priv, key_pub):
+ pointP = key_pub.pointQ * key_priv.d
+ if pointP.is_point_at_infinity():
+ raise ValueError("Invalid ECDH point")
+
+ if key_priv.curve == "Curve25519":
+ z = bytearray(pointP.x.to_bytes(32, byteorder='little'))
+ elif key_priv.curve == "Curve448":
+ z = bytearray(pointP.x.to_bytes(56, byteorder='little'))
+ else:
+ # See Section 5.7.1.2 in NIST SP 800-56Ar3
+ z = long_to_bytes(pointP.x, pointP.size_in_bytes())
+ return z
+
+
+def import_x25519_public_key(encoded):
+ """Create a new X25519 public key object,
+ starting from the key encoded as raw ``bytes``,
+ in the format described in RFC7748.
+
+ Args:
+ encoded (bytes):
+ The x25519 public key to import.
+ It must be 32 bytes.
+
+ Returns:
+ :class:`Crypto.PublicKey.EccKey` : a new ECC key object.
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+ """
+
+ x = _import_curve25519_public_key(encoded)
+ return construct(curve='Curve25519', point_x=x)
+
+
+def import_x25519_private_key(encoded):
+ """Create a new X25519 private key object,
+ starting from the key encoded as raw ``bytes``,
+ in the format described in RFC7748.
+
+ Args:
+ encoded (bytes):
+ The X25519 private key to import.
+ It must be 32 bytes.
+
+ Returns:
+ :class:`Crypto.PublicKey.EccKey` : a new ECC key object.
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+ """
+
+ return construct(seed=encoded, curve="Curve25519")
+
+
+def import_x448_public_key(encoded):
+ """Create a new X448 public key object,
+ starting from the key encoded as raw ``bytes``,
+ in the format described in RFC7748.
+
+ Args:
+ encoded (bytes):
+ The x448 public key to import.
+ It must be 56 bytes.
+
+ Returns:
+ :class:`Crypto.PublicKey.EccKey` : a new ECC key object.
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+ """
+
+ x = _import_curve448_public_key(encoded)
+ return construct(curve='Curve448', point_x=x)
+
+
+def import_x448_private_key(encoded):
+ """Create a new X448 private key object,
+ starting from the key encoded as raw ``bytes``,
+ in the format described in RFC7748.
+
+ Args:
+ encoded (bytes):
+ The X448 private key to import.
+ It must be 56 bytes.
+
+ Returns:
+ :class:`Crypto.PublicKey.EccKey` : a new ECC key object.
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+ """
+
+ return construct(seed=encoded, curve="Curve448")
+
+
+def key_agreement(**kwargs):
+ """Perform a Diffie-Hellman key agreement.
+
+ Keywords:
+ kdf (callable):
+ A key derivation function that accepts ``bytes`` as input and returns
+ ``bytes``.
+ static_priv (EccKey):
+ The local static private key. Optional.
+ static_pub (EccKey):
+ The static public key that belongs to the peer. Optional.
+ eph_priv (EccKey):
+ The local ephemeral private key, generated for this session. Optional.
+ eph_pub (EccKey):
+ The ephemeral public key, received from the peer for this session. Optional.
+
+ At least two keys must be passed, of which one is a private key and one
+ a public key.
+
+ Returns (bytes):
+ The derived secret key material.
+ """
+
+ static_priv = kwargs.get('static_priv', None)
+ static_pub = kwargs.get('static_pub', None)
+ eph_priv = kwargs.get('eph_priv', None)
+ eph_pub = kwargs.get('eph_pub', None)
+ kdf = kwargs.get('kdf', None)
+
+ if kdf is None:
+ raise ValueError("'kdf' is mandatory")
+
+ count_priv = 0
+ count_pub = 0
+ curve = None
+
+ def check_curve(curve, key, name, private):
+ if not isinstance(key, EccKey):
+ raise TypeError("'%s' must be an ECC key" % name)
+ if private and not key.has_private():
+ raise TypeError("'%s' must be a private ECC key" % name)
+ if curve is None:
+ curve = key.curve
+ elif curve != key.curve:
+ raise TypeError("'%s' is defined on an incompatible curve" % name)
+ return curve
+
+ if static_priv is not None:
+ curve = check_curve(curve, static_priv, 'static_priv', True)
+ count_priv += 1
+
+ if static_pub is not None:
+ curve = check_curve(curve, static_pub, 'static_pub', False)
+ count_pub += 1
+
+ if eph_priv is not None:
+ curve = check_curve(curve, eph_priv, 'eph_priv', True)
+ count_priv += 1
+
+ if eph_pub is not None:
+ curve = check_curve(curve, eph_pub, 'eph_pub', False)
+ count_pub += 1
+
+ if (count_priv + count_pub) < 2 or count_priv == 0 or count_pub == 0:
+ raise ValueError("Too few keys for the ECDH key agreement")
+
+ Zs = b''
+ Ze = b''
+
+ if static_priv and static_pub:
+ # C(*, 2s)
+ Zs = _compute_ecdh(static_priv, static_pub)
+
+ if eph_priv and eph_pub:
+ # C(2e, 0s) or C(2e, 2s)
+ if bool(static_priv) != bool(static_pub):
+ raise ValueError("DH mode C(2e, 1s) is not supported")
+ Ze = _compute_ecdh(eph_priv, eph_pub)
+ elif eph_priv and static_pub:
+ # C(1e, 2s) or C(1e, 1s)
+ Ze = _compute_ecdh(eph_priv, static_pub)
+ elif eph_pub and static_priv:
+ # C(1e, 2s) or C(1e, 1s)
+ Ze = _compute_ecdh(static_priv, eph_pub)
+
+ Z = Ze + Zs
+
+ return kdf(Z)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/DH.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/DH.pyi
new file mode 100644
index 00000000..634a0072
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/DH.pyi
@@ -0,0 +1,19 @@
+from typing import TypedDict, Callable, TypeVar, Generic
+from typing_extensions import Unpack, NotRequired
+
+from Crypto.PublicKey.ECC import EccKey
+
+T = TypeVar('T')
+
+class RequestParams(TypedDict, Generic[T]):
+ kdf: Callable[[bytes|bytearray|memoryview], T]
+ static_priv: NotRequired[EccKey]
+ static_pub: NotRequired[EccKey]
+ eph_priv: NotRequired[EccKey]
+ eph_pub: NotRequired[EccKey]
+
+def import_x25519_public_key(encoded: bytes) -> EccKey: ...
+def import_x25519_private_key(encoded: bytes) -> EccKey: ...
+def import_x448_public_key(encoded: bytes) -> EccKey: ...
+def import_x448_private_key(encoded: bytes) -> EccKey: ...
+def key_agreement(**kwargs: Unpack[RequestParams[T]]) -> T: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/HPKE.py b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/HPKE.py
new file mode 100644
index 00000000..df314439
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/HPKE.py
@@ -0,0 +1,483 @@
+import struct
+from enum import IntEnum
+
+from types import ModuleType
+from typing import Optional
+
+from .KDF import _HKDF_extract, _HKDF_expand
+from .DH import key_agreement, import_x25519_public_key, import_x448_public_key
+from Crypto.Util.strxor import strxor
+from Crypto.PublicKey import ECC
+from Crypto.PublicKey.ECC import EccKey
+from Crypto.Hash import SHA256, SHA384, SHA512
+from Crypto.Cipher import AES, ChaCha20_Poly1305
+
+
+class MODE(IntEnum):
+ """HPKE modes"""
+ BASE = 0x00
+ PSK = 0x01
+ AUTH = 0x02
+ AUTH_PSK = 0x03
+
+
+class AEAD(IntEnum):
+ """Authenticated Encryption with Associated Data (AEAD) Functions"""
+ AES128_GCM = 0x0001
+ AES256_GCM = 0x0002
+ CHACHA20_POLY1305 = 0x0003
+
+
+class DeserializeError(ValueError):
+ pass
+
+class MessageLimitReachedError(ValueError):
+ pass
+
+# CURVE to (KEM ID, KDF ID, HASH)
+_Curve_Config = {
+ "NIST P-256": (0x0010, 0x0001, SHA256),
+ "NIST P-384": (0x0011, 0x0002, SHA384),
+ "NIST P-521": (0x0012, 0x0003, SHA512),
+ "Curve25519": (0x0020, 0x0001, SHA256),
+ "Curve448": (0x0021, 0x0003, SHA512),
+}
+
+
+def _labeled_extract(salt: bytes,
+ label: bytes,
+ ikm: bytes,
+ suite_id: bytes,
+ hashmod: ModuleType):
+ labeled_ikm = b"HPKE-v1" + suite_id + label + ikm
+ return _HKDF_extract(salt, labeled_ikm, hashmod)
+
+
+def _labeled_expand(prk: bytes,
+ label: bytes,
+ info: bytes,
+ L: int,
+ suite_id: bytes,
+ hashmod: ModuleType):
+ labeled_info = struct.pack('>H', L) + b"HPKE-v1" + suite_id + \
+ label + info
+ return _HKDF_expand(prk, labeled_info, L, hashmod)
+
+
+def _extract_and_expand(dh: bytes,
+ kem_context: bytes,
+ suite_id: bytes,
+ hashmod: ModuleType):
+ Nsecret = hashmod.digest_size
+
+ eae_prk = _labeled_extract(b"",
+ b"eae_prk",
+ dh,
+ suite_id,
+ hashmod)
+
+ shared_secret = _labeled_expand(eae_prk,
+ b"shared_secret",
+ kem_context,
+ Nsecret,
+ suite_id,
+ hashmod)
+ return shared_secret
+
+
+class HPKE_Cipher:
+
+ def __init__(self,
+ receiver_key: EccKey,
+ enc: Optional[bytes],
+ sender_key: Optional[EccKey],
+ psk_pair: tuple[bytes, bytes],
+ info: bytes,
+ aead_id: AEAD,
+ mode: MODE):
+
+ self.enc: bytes = b'' if enc is None else enc
+ """The encapsulated session key."""
+
+ self._verify_psk_inputs(mode, psk_pair)
+
+ self._curve = receiver_key.curve
+ self._aead_id = aead_id
+ self._mode = mode
+
+ try:
+ self._kem_id, \
+ self._kdf_id, \
+ self._hashmod = _Curve_Config[self._curve]
+ except KeyError as ke:
+ raise ValueError("Curve {} is not supported by HPKE".format(self._curve)) from ke
+
+ self._Nk = 16 if self._aead_id == AEAD.AES128_GCM else 32
+ self._Nn = 12
+ self._Nt = 16
+ self._Nh = self._hashmod.digest_size
+
+ self._encrypt = not receiver_key.has_private()
+
+ if self._encrypt:
+ # SetupBaseS (encryption)
+ if enc is not None:
+ raise ValueError("Parameter 'enc' cannot be an input when sealing")
+ shared_secret, self.enc = self._encap(receiver_key,
+ self._kem_id,
+ self._hashmod,
+ sender_key)
+ else:
+ # SetupBaseR (decryption)
+ if enc is None:
+ raise ValueError("Parameter 'enc' required when unsealing")
+ shared_secret = self._decap(enc,
+ receiver_key,
+ self._kem_id,
+ self._hashmod,
+ sender_key)
+
+ self._sequence = 0
+ self._max_sequence = (1 << (8 * self._Nn)) - 1
+
+ self._key, \
+ self._base_nonce, \
+ self._export_secret = self._key_schedule(shared_secret,
+ info,
+ *psk_pair)
+
+ @staticmethod
+ def _encap(receiver_key: EccKey,
+ kem_id: int,
+ hashmod: ModuleType,
+ sender_key: Optional[EccKey] = None,
+ eph_key: Optional[EccKey] = None):
+
+ assert (sender_key is None) or sender_key.has_private()
+ assert (eph_key is None) or eph_key.has_private()
+
+ if eph_key is None:
+ eph_key = ECC.generate(curve=receiver_key.curve)
+ enc = eph_key.public_key().export_key(format='raw')
+
+ pkRm = receiver_key.public_key().export_key(format='raw')
+ kem_context = enc + pkRm
+ extra_param = {}
+ if sender_key:
+ kem_context += sender_key.public_key().export_key(format='raw')
+ extra_param = {'static_priv': sender_key}
+
+ suite_id = b"KEM" + struct.pack('>H', kem_id)
+
+ def kdf(dh,
+ kem_context=kem_context,
+ suite_id=suite_id,
+ hashmod=hashmod):
+ return _extract_and_expand(dh, kem_context, suite_id, hashmod)
+
+ shared_secret = key_agreement(eph_priv=eph_key,
+ static_pub=receiver_key,
+ kdf=kdf,
+ **extra_param)
+ return shared_secret, enc
+
+ @staticmethod
+ def _decap(enc: bytes,
+ receiver_key: EccKey,
+ kem_id: int,
+ hashmod: ModuleType,
+ sender_key: Optional[EccKey] = None):
+
+ assert receiver_key.has_private()
+
+ try:
+ if receiver_key.curve == 'Curve25519':
+ pkE = import_x25519_public_key(enc)
+ elif receiver_key.curve == 'Curve448':
+ pkE = import_x448_public_key(enc)
+ else:
+ pkE = ECC.import_key(enc, curve_name=receiver_key.curve)
+ except ValueError as ve:
+ raise DeserializeError("'enc' is not a valid encapsulated HPKE key") from ve
+
+ pkRm = receiver_key.public_key().export_key(format='raw')
+ kem_context = enc + pkRm
+ extra_param = {}
+ if sender_key:
+ kem_context += sender_key.public_key().export_key(format='raw')
+ extra_param = {'static_pub': sender_key}
+
+ suite_id = b"KEM" + struct.pack('>H', kem_id)
+
+ def kdf(dh,
+ kem_context=kem_context,
+ suite_id=suite_id,
+ hashmod=hashmod):
+ return _extract_and_expand(dh, kem_context, suite_id, hashmod)
+
+ shared_secret = key_agreement(eph_pub=pkE,
+ static_priv=receiver_key,
+ kdf=kdf,
+ **extra_param)
+ return shared_secret
+
+ @staticmethod
+ def _verify_psk_inputs(mode: MODE, psk_pair: tuple[bytes, bytes]):
+ psk_id, psk = psk_pair
+
+ if (psk == b'') ^ (psk_id == b''):
+ raise ValueError("Inconsistent PSK inputs")
+
+ if (psk == b''):
+ if mode in (MODE.PSK, MODE.AUTH_PSK):
+ raise ValueError(f"PSK is required with mode {mode.name}")
+ else:
+ if len(psk) < 32:
+ raise ValueError("PSK must be at least 32 byte long")
+ if mode in (MODE.BASE, MODE.AUTH):
+ raise ValueError("PSK is not compatible with this mode")
+
+ def _key_schedule(self,
+ shared_secret: bytes,
+ info: bytes,
+ psk_id: bytes,
+ psk: bytes):
+
+ suite_id = b"HPKE" + struct.pack('>HHH',
+ self._kem_id,
+ self._kdf_id,
+ self._aead_id)
+
+ psk_id_hash = _labeled_extract(b'',
+ b'psk_id_hash',
+ psk_id,
+ suite_id,
+ self._hashmod)
+
+ info_hash = _labeled_extract(b'',
+ b'info_hash',
+ info,
+ suite_id,
+ self._hashmod)
+
+ key_schedule_context = self._mode.to_bytes(1, 'big') + psk_id_hash + info_hash
+
+ secret = _labeled_extract(shared_secret,
+ b'secret',
+ psk,
+ suite_id,
+ self._hashmod)
+
+ key = _labeled_expand(secret,
+ b'key',
+ key_schedule_context,
+ self._Nk,
+ suite_id,
+ self._hashmod)
+
+ base_nonce = _labeled_expand(secret,
+ b'base_nonce',
+ key_schedule_context,
+ self._Nn,
+ suite_id,
+ self._hashmod)
+
+ exporter_secret = _labeled_expand(secret,
+ b'exp',
+ key_schedule_context,
+ self._Nh,
+ suite_id,
+ self._hashmod)
+
+ return key, base_nonce, exporter_secret
+
+ def _new_cipher(self):
+ nonce = strxor(self._base_nonce, self._sequence.to_bytes(self._Nn, 'big'))
+ if self._aead_id in (AEAD.AES128_GCM, AEAD.AES256_GCM):
+ cipher = AES.new(self._key, AES.MODE_GCM, nonce=nonce, mac_len=self._Nt)
+ elif self._aead_id == AEAD.CHACHA20_POLY1305:
+ cipher = ChaCha20_Poly1305.new(key=self._key, nonce=nonce)
+ else:
+ raise ValueError(f"Unknown AEAD cipher ID {self._aead_id:#x}")
+ if self._sequence >= self._max_sequence:
+ raise MessageLimitReachedError()
+ self._sequence += 1
+ return cipher
+
+ def seal(self, plaintext: bytes, auth_data: Optional[bytes] = None):
+ """Encrypt and authenticate a message.
+
+ This method can be invoked multiple times
+ to seal an ordered sequence of messages.
+
+ Arguments:
+ plaintext: bytes
+ The message to seal.
+ auth_data: bytes
+ Optional. Additional Authenticated data (AAD) that is not encrypted
+ but that will be also covered by the authentication tag.
+
+ Returns:
+ The ciphertext concatenated with the authentication tag.
+ """
+
+ if not self._encrypt:
+ raise ValueError("This cipher can only be used to seal")
+ cipher = self._new_cipher()
+ if auth_data:
+ cipher.update(auth_data)
+ ct, tag = cipher.encrypt_and_digest(plaintext)
+ return ct + tag
+
+ def unseal(self, ciphertext: bytes, auth_data: Optional[bytes] = None):
+ """Decrypt a message and validate its authenticity.
+
+ This method can be invoked multiple times
+ to unseal an ordered sequence of messages.
+
+ Arguments:
+ cipertext: bytes
+ The message to unseal.
+ auth_data: bytes
+ Optional. Additional Authenticated data (AAD) that
+ was also covered by the authentication tag.
+
+ Returns:
+ The original plaintext.
+
+ Raises: ValueError
+ If the ciphertext (in combination with the AAD) is not valid.
+
+ But if it is the first time you call ``unseal()`` this
+ exception may also mean that any of the parameters or keys
+ used to establish the session is wrong or that one is missing.
+ """
+
+ if self._encrypt:
+ raise ValueError("This cipher can only be used to unseal")
+ if len(ciphertext) < self._Nt:
+ raise ValueError("Ciphertext is too small")
+ cipher = self._new_cipher()
+ if auth_data:
+ cipher.update(auth_data)
+
+ try:
+ pt = cipher.decrypt_and_verify(ciphertext[:-self._Nt],
+ ciphertext[-self._Nt:])
+ except ValueError:
+ if self._sequence == 1:
+ raise ValueError("Incorrect HPKE keys/parameters or invalid message (wrong MAC tag)")
+ raise ValueError("Invalid message (wrong MAC tag)")
+ return pt
+
+
+def new(*, receiver_key: EccKey,
+ aead_id: AEAD,
+ enc: Optional[bytes] = None,
+ sender_key: Optional[EccKey] = None,
+ psk: Optional[tuple[bytes, bytes]] = None,
+ info: Optional[bytes] = None) -> HPKE_Cipher:
+ """Create an HPKE context which can be used:
+
+ - by the sender to seal (encrypt) a message or
+ - by the receiver to unseal (decrypt) it.
+
+ As a minimum, the two parties agree on the receiver's asymmetric key
+ (of which the sender will only know the public half).
+
+ Additionally, for authentication purposes, they may also agree on:
+
+ * the sender's asymmetric key (of which the receiver will only know the public half)
+
+ * a shared secret (e.g., a symmetric key derived from a password)
+
+ Args:
+ receiver_key:
+ The ECC key of the receiver.
+ It must be on one of the following curves: ``NIST P-256``,
+ ``NIST P-384``, ``NIST P-521``, ``X25519`` or ``X448``.
+
+ If this is a **public** key, the HPKE context can only be used to
+ **seal** (**encrypt**).
+
+ If this is a **private** key, the HPKE context can only be used to
+ **unseal** (**decrypt**).
+
+ aead_id:
+ The HPKE identifier of the symmetric cipher.
+ The possible values are:
+
+ * ``HPKE.AEAD.AES128_GCM``
+ * ``HPKE.AEAD.AES256_GCM``
+ * ``HPKE.AEAD.CHACHA20_POLY1305``
+
+ enc:
+ The encapsulated session key (i.e., the KEM shared secret).
+
+ The receiver must always specify this parameter.
+
+ The sender must always omit this parameter.
+
+ sender_key:
+ The ECC key of the sender.
+ It must be on the same curve as the ``receiver_key``.
+ If the ``receiver_key`` is a public key, ``sender_key`` must be a
+ private key, and vice versa.
+
+ psk:
+ A Pre-Shared Key (PSK) as a 2-tuple of non-empty
+ byte strings: the identifier and the actual secret value.
+ Sender and receiver must use the same PSK (or none).
+
+ The secret value must be at least 32 bytes long,
+ but it must not be a low-entropy password
+ (use a KDF like PBKDF2 or scrypt to derive a secret
+ from a password).
+
+ info:
+ A non-secret parameter that contributes
+ to the generation of all session keys.
+ Sender and receive must use the same **info** parameter (or none).
+
+ Returns:
+ An object that can be used for
+ sealing (if ``receiver_key`` is a public key) or
+ unsealing (if ``receiver_key`` is a private key).
+ In the latter case,
+ correctness of all the keys and parameters will only
+ be assessed with the first call to ``unseal()``.
+ """
+
+ if aead_id not in AEAD:
+ raise ValueError(f"Unknown AEAD cipher ID {aead_id:#x}")
+
+ curve = receiver_key.curve
+ if curve not in ('NIST P-256', 'NIST P-384', 'NIST P-521',
+ 'Curve25519', 'Curve448'):
+ raise ValueError(f"Unsupported curve {curve}")
+
+ if sender_key:
+ count_private_keys = int(receiver_key.has_private()) + \
+ int(sender_key.has_private())
+ if count_private_keys != 1:
+ raise ValueError("Exactly 1 private key required")
+ if sender_key.curve != curve:
+ raise ValueError("Sender key uses {} but recipient key {}".
+ format(sender_key.curve, curve))
+ mode = MODE.AUTH if psk is None else MODE.AUTH_PSK
+ else:
+ mode = MODE.BASE if psk is None else MODE.PSK
+
+ if psk is None:
+ psk = b'', b''
+
+ if info is None:
+ info = b''
+
+ return HPKE_Cipher(receiver_key,
+ enc,
+ sender_key,
+ psk,
+ info,
+ aead_id,
+ mode)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/KDF.py b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/KDF.py
new file mode 100644
index 00000000..cca00694
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/KDF.py
@@ -0,0 +1,647 @@
+# coding=utf-8
+#
+# KDF.py : a collection of Key Derivation Functions
+#
+# Part of the Python Cryptography Toolkit
+#
+# ===================================================================
+# 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 re
+import struct
+from functools import reduce
+
+from Crypto.Util.py3compat import (tobytes, bord, _copy_bytes, iter_range,
+ tostr, bchr, bstr)
+
+from Crypto.Hash import SHA1, SHA256, HMAC, CMAC, BLAKE2s
+from Crypto.Util.strxor import strxor
+from Crypto.Random import get_random_bytes
+from Crypto.Util.number import size as bit_size, long_to_bytes, bytes_to_long
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ create_string_buffer,
+ get_raw_buffer, c_size_t)
+
+_raw_salsa20_lib = load_pycryptodome_raw_lib(
+ "Crypto.Cipher._Salsa20",
+ """
+ int Salsa20_8_core(const uint8_t *x, const uint8_t *y,
+ uint8_t *out);
+ """)
+
+_raw_scrypt_lib = load_pycryptodome_raw_lib(
+ "Crypto.Protocol._scrypt",
+ """
+ typedef int (core_t)(const uint8_t [64], const uint8_t [64], uint8_t [64]);
+ int scryptROMix(const uint8_t *data_in, uint8_t *data_out,
+ size_t data_len, unsigned N, core_t *core);
+ """)
+
+
+def PBKDF1(password, salt, dkLen, count=1000, hashAlgo=None):
+ """Derive one key from a password (or passphrase).
+
+ This function performs key derivation according to an old version of
+ the PKCS#5 standard (v1.5) or `RFC2898
+ `_.
+
+ Args:
+ password (string):
+ The secret password to generate the key from.
+ salt (byte string):
+ An 8 byte string to use for better protection from dictionary attacks.
+ This value does not need to be kept secret, but it should be randomly
+ chosen for each derivation.
+ dkLen (integer):
+ The length of the desired key. The default is 16 bytes, suitable for
+ instance for :mod:`Crypto.Cipher.AES`.
+ count (integer):
+ The number of iterations to carry out. The recommendation is 1000 or
+ more.
+ hashAlgo (module):
+ The hash algorithm to use, as a module or an object from the :mod:`Crypto.Hash` package.
+ The digest length must be no shorter than ``dkLen``.
+ The default algorithm is :mod:`Crypto.Hash.SHA1`.
+
+ Return:
+ A byte string of length ``dkLen`` that can be used as key.
+ """
+
+ if not hashAlgo:
+ hashAlgo = SHA1
+ password = tobytes(password)
+ pHash = hashAlgo.new(password+salt)
+ digest = pHash.digest_size
+ if dkLen > digest:
+ raise TypeError("Selected hash algorithm has a too short digest (%d bytes)." % digest)
+ if len(salt) != 8:
+ raise ValueError("Salt is not 8 bytes long (%d bytes instead)." % len(salt))
+ for i in iter_range(count-1):
+ pHash = pHash.new(pHash.digest())
+ return pHash.digest()[:dkLen]
+
+
+def PBKDF2(password, salt, dkLen=16, count=1000, prf=None, hmac_hash_module=None):
+ """Derive one or more keys from a password (or passphrase).
+
+ This function performs key derivation according to the PKCS#5 standard (v2.0).
+
+ Args:
+ password (string or byte string):
+ The secret password to generate the key from.
+
+ Strings will be encoded as ISO 8859-1 (also known as Latin-1),
+ which does not allow any characters with codepoints > 255.
+ salt (string or byte string):
+ A (byte) string to use for better protection from dictionary attacks.
+ This value does not need to be kept secret, but it should be randomly
+ chosen for each derivation. It is recommended to use at least 16 bytes.
+
+ Strings will be encoded as ISO 8859-1 (also known as Latin-1),
+ which does not allow any characters with codepoints > 255.
+ dkLen (integer):
+ The cumulative length of the keys to produce.
+
+ Due to a flaw in the PBKDF2 design, you should not request more bytes
+ than the ``prf`` can output. For instance, ``dkLen`` should not exceed
+ 20 bytes in combination with ``HMAC-SHA1``.
+ count (integer):
+ The number of iterations to carry out. The higher the value, the slower
+ and the more secure the function becomes.
+
+ You should find the maximum number of iterations that keeps the
+ key derivation still acceptable on the slowest hardware you must support.
+
+ Although the default value is 1000, **it is recommended to use at least
+ 1000000 (1 million) iterations**.
+ prf (callable):
+ A pseudorandom function. It must be a function that returns a
+ pseudorandom byte string from two parameters: a secret and a salt.
+ The slower the algorithm, the more secure the derivation function.
+ If not specified, **HMAC-SHA1** is used.
+ hmac_hash_module (module):
+ A module from ``Crypto.Hash`` implementing a Merkle-Damgard cryptographic
+ hash, which PBKDF2 must use in combination with HMAC.
+ This parameter is mutually exclusive with ``prf``.
+
+ Return:
+ A byte string of length ``dkLen`` that can be used as key material.
+ If you want multiple keys, just break up this string into segments of the desired length.
+ """
+
+ password = tobytes(password)
+ salt = tobytes(salt)
+
+ if prf and hmac_hash_module:
+ raise ValueError("'prf' and 'hmac_hash_module' are mutually exlusive")
+
+ if prf is None and hmac_hash_module is None:
+ hmac_hash_module = SHA1
+
+ if prf or not hasattr(hmac_hash_module, "_pbkdf2_hmac_assist"):
+ # Generic (and slow) implementation
+
+ if prf is None:
+ prf = lambda p, s: HMAC.new(p, s, hmac_hash_module).digest()
+
+ def link(s):
+ s[0], s[1] = s[1], prf(password, s[1])
+ return s[0]
+
+ key = b''
+ i = 1
+ while len(key) < dkLen:
+ s = [prf(password, salt + struct.pack(">I", i))] * 2
+ key += reduce(strxor, (link(s) for j in range(count)))
+ i += 1
+
+ else:
+ # Optimized implementation
+ key = b''
+ i = 1
+ while len(key) < dkLen:
+ base = HMAC.new(password, b"", hmac_hash_module)
+ first_digest = base.copy().update(salt + struct.pack(">I", i)).digest()
+ key += base._pbkdf2_hmac_assist(first_digest, count)
+ i += 1
+
+ return key[:dkLen]
+
+
+class _S2V(object):
+ """String-to-vector PRF as defined in `RFC5297`_.
+
+ This class implements a pseudorandom function family
+ based on CMAC that takes as input a vector of strings.
+
+ .. _RFC5297: http://tools.ietf.org/html/rfc5297
+ """
+
+ def __init__(self, key, ciphermod, cipher_params=None):
+ """Initialize the S2V PRF.
+
+ :Parameters:
+ key : byte string
+ A secret that can be used as key for CMACs
+ based on ciphers from ``ciphermod``.
+ ciphermod : module
+ A block cipher module from `Crypto.Cipher`.
+ cipher_params : dictionary
+ A set of extra parameters to use to create a cipher instance.
+ """
+
+ self._key = _copy_bytes(None, None, key)
+ self._ciphermod = ciphermod
+ self._last_string = self._cache = b'\x00' * ciphermod.block_size
+
+ # Max number of update() call we can process
+ self._n_updates = ciphermod.block_size * 8 - 1
+
+ if cipher_params is None:
+ self._cipher_params = {}
+ else:
+ self._cipher_params = dict(cipher_params)
+
+ @staticmethod
+ def new(key, ciphermod):
+ """Create a new S2V PRF.
+
+ :Parameters:
+ key : byte string
+ A secret that can be used as key for CMACs
+ based on ciphers from ``ciphermod``.
+ ciphermod : module
+ A block cipher module from `Crypto.Cipher`.
+ """
+ return _S2V(key, ciphermod)
+
+ def _double(self, bs):
+ doubled = bytes_to_long(bs) << 1
+ if bord(bs[0]) & 0x80:
+ doubled ^= 0x87
+ return long_to_bytes(doubled, len(bs))[-len(bs):]
+
+ def update(self, item):
+ """Pass the next component of the vector.
+
+ The maximum number of components you can pass is equal to the block
+ length of the cipher (in bits) minus 1.
+
+ :Parameters:
+ item : byte string
+ The next component of the vector.
+ :Raise TypeError: when the limit on the number of components has been reached.
+ """
+
+ if self._n_updates == 0:
+ raise TypeError("Too many components passed to S2V")
+ self._n_updates -= 1
+
+ mac = CMAC.new(self._key,
+ msg=self._last_string,
+ ciphermod=self._ciphermod,
+ cipher_params=self._cipher_params)
+ self._cache = strxor(self._double(self._cache), mac.digest())
+ self._last_string = _copy_bytes(None, None, item)
+
+ def derive(self):
+ """"Derive a secret from the vector of components.
+
+ :Return: a byte string, as long as the block length of the cipher.
+ """
+
+ if len(self._last_string) >= 16:
+ # xorend
+ final = self._last_string[:-16] + strxor(self._last_string[-16:], self._cache)
+ else:
+ # zero-pad & xor
+ padded = (self._last_string + b'\x80' + b'\x00' * 15)[:16]
+ final = strxor(padded, self._double(self._cache))
+ mac = CMAC.new(self._key,
+ msg=final,
+ ciphermod=self._ciphermod,
+ cipher_params=self._cipher_params)
+ return mac.digest()
+
+
+def _HKDF_extract(salt, ikm, hashmod):
+ prk = HMAC.new(salt, ikm, digestmod=hashmod).digest()
+ return prk
+
+
+def _HKDF_expand(prk, info, L, hashmod):
+ t = [b""]
+ n = 1
+ tlen = 0
+ while tlen < L:
+ hmac = HMAC.new(prk, t[-1] + info + struct.pack('B', n), digestmod=hashmod)
+ t.append(hmac.digest())
+ tlen += hashmod.digest_size
+ n += 1
+ okm = b"".join(t)
+ return okm[:L]
+
+
+def HKDF(master, key_len, salt, hashmod, num_keys=1, context=None):
+ """Derive one or more keys from a master secret using
+ the HMAC-based KDF defined in RFC5869_.
+
+ Args:
+ master (byte string):
+ The unguessable value used by the KDF to generate the other keys.
+ It must be a high-entropy secret, though not necessarily uniform.
+ It must not be a password.
+ key_len (integer):
+ The length in bytes of every derived key.
+ salt (byte string):
+ A non-secret, reusable value that strengthens the randomness
+ extraction step.
+ Ideally, it is as long as the digest size of the chosen hash.
+ If empty, a string of zeroes in used.
+ hashmod (module):
+ A cryptographic hash algorithm from :mod:`Crypto.Hash`.
+ :mod:`Crypto.Hash.SHA512` is a good choice.
+ num_keys (integer):
+ The number of keys to derive. Every key is :data:`key_len` bytes long.
+ The maximum cumulative length of all keys is
+ 255 times the digest size.
+ context (byte string):
+ Optional identifier describing what the keys are used for.
+
+ Return:
+ A byte string or a tuple of byte strings.
+
+ .. _RFC5869: http://tools.ietf.org/html/rfc5869
+ """
+
+ output_len = key_len * num_keys
+ if output_len > (255 * hashmod.digest_size):
+ raise ValueError("Too much secret data to derive")
+ if not salt:
+ salt = b'\x00' * hashmod.digest_size
+ if context is None:
+ context = b""
+
+ prk = _HKDF_extract(salt, master, hashmod)
+ okm = _HKDF_expand(prk, context, output_len, hashmod)
+
+ if num_keys == 1:
+ return okm[:key_len]
+ kol = [okm[idx:idx + key_len]
+ for idx in iter_range(0, output_len, key_len)]
+ return list(kol[:num_keys])
+
+
+def scrypt(password, salt, key_len, N, r, p, num_keys=1):
+ """Derive one or more keys from a passphrase.
+
+ Args:
+ password (string):
+ The secret pass phrase to generate the keys from.
+ salt (string):
+ A string to use for better protection from dictionary attacks.
+ This value does not need to be kept secret,
+ but it should be randomly chosen for each derivation.
+ It is recommended to be at least 16 bytes long.
+ key_len (integer):
+ The length in bytes of each derived key.
+ N (integer):
+ CPU/Memory cost parameter. It must be a power of 2 and less
+ than :math:`2^{32}`.
+ r (integer):
+ Block size parameter.
+ p (integer):
+ Parallelization parameter.
+ It must be no greater than :math:`(2^{32}-1)/(4r)`.
+ num_keys (integer):
+ The number of keys to derive. Every key is :data:`key_len` bytes long.
+ By default, only 1 key is generated.
+ The maximum cumulative length of all keys is :math:`(2^{32}-1)*32`
+ (that is, 128TB).
+
+ A good choice of parameters *(N, r , p)* was suggested
+ by Colin Percival in his `presentation in 2009`__:
+
+ - *( 2¹⁴, 8, 1 )* for interactive logins (≤100ms)
+ - *( 2²⁰, 8, 1 )* for file encryption (≤5s)
+
+ Return:
+ A byte string or a tuple of byte strings.
+
+ .. __: http://www.tarsnap.com/scrypt/scrypt-slides.pdf
+ """
+
+ if 2 ** (bit_size(N) - 1) != N:
+ raise ValueError("N must be a power of 2")
+ if N >= 2 ** 32:
+ raise ValueError("N is too big")
+ if p > ((2 ** 32 - 1) * 32) // (128 * r):
+ raise ValueError("p or r are too big")
+
+ prf_hmac_sha256 = lambda p, s: HMAC.new(p, s, SHA256).digest()
+
+ stage_1 = PBKDF2(password, salt, p * 128 * r, 1, prf=prf_hmac_sha256)
+
+ scryptROMix = _raw_scrypt_lib.scryptROMix
+ core = _raw_salsa20_lib.Salsa20_8_core
+
+ # Parallelize into p flows
+ data_out = []
+ for flow in iter_range(p):
+ idx = flow * 128 * r
+ buffer_out = create_string_buffer(128 * r)
+ result = scryptROMix(stage_1[idx: idx + 128 * r],
+ buffer_out,
+ c_size_t(128 * r),
+ N,
+ core)
+ if result:
+ raise ValueError("Error %X while running scrypt" % result)
+ data_out += [get_raw_buffer(buffer_out)]
+
+ dk = PBKDF2(password,
+ b"".join(data_out),
+ key_len * num_keys, 1,
+ prf=prf_hmac_sha256)
+
+ if num_keys == 1:
+ return dk
+
+ kol = [dk[idx:idx + key_len]
+ for idx in iter_range(0, key_len * num_keys, key_len)]
+ return kol
+
+
+def _bcrypt_encode(data):
+ s = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+
+ bits = []
+ for c in data:
+ bits_c = bin(bord(c))[2:].zfill(8)
+ bits.append(bstr(bits_c))
+ bits = b"".join(bits)
+
+ bits6 = [bits[idx:idx+6] for idx in range(0, len(bits), 6)]
+
+ result = []
+ for g in bits6[:-1]:
+ idx = int(g, 2)
+ result.append(s[idx])
+
+ g = bits6[-1]
+ idx = int(g, 2) << (6 - len(g))
+ result.append(s[idx])
+ result = "".join(result)
+
+ return tobytes(result)
+
+
+def _bcrypt_decode(data):
+ s = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+
+ bits = []
+ for c in tostr(data):
+ idx = s.find(c)
+ bits6 = bin(idx)[2:].zfill(6)
+ bits.append(bits6)
+ bits = "".join(bits)
+
+ modulo4 = len(data) % 4
+ if modulo4 == 1:
+ raise ValueError("Incorrect length")
+ elif modulo4 == 2:
+ bits = bits[:-4]
+ elif modulo4 == 3:
+ bits = bits[:-2]
+
+ bits8 = [bits[idx:idx+8] for idx in range(0, len(bits), 8)]
+
+ result = []
+ for g in bits8:
+ result.append(bchr(int(g, 2)))
+ result = b"".join(result)
+
+ return result
+
+
+def _bcrypt_hash(password, cost, salt, constant, invert):
+ from Crypto.Cipher import _EKSBlowfish
+
+ if len(password) > 72:
+ raise ValueError("The password is too long. It must be 72 bytes at most.")
+
+ if not (4 <= cost <= 31):
+ raise ValueError("bcrypt cost factor must be in the range 4..31")
+
+ cipher = _EKSBlowfish.new(password, _EKSBlowfish.MODE_ECB, salt, cost, invert)
+ ctext = constant
+ for _ in range(64):
+ ctext = cipher.encrypt(ctext)
+ return ctext
+
+
+def bcrypt(password, cost, salt=None):
+ """Hash a password into a key, using the OpenBSD bcrypt protocol.
+
+ Args:
+ password (byte string or string):
+ The secret password or pass phrase.
+ It must be at most 72 bytes long.
+ It must not contain the zero byte.
+ Unicode strings will be encoded as UTF-8.
+ cost (integer):
+ The exponential factor that makes it slower to compute the hash.
+ It must be in the range 4 to 31.
+ A value of at least 12 is recommended.
+ salt (byte string):
+ Optional. Random byte string to thwarts dictionary and rainbow table
+ attacks. It must be 16 bytes long.
+ If not passed, a random value is generated.
+
+ Return (byte string):
+ The bcrypt hash
+
+ Raises:
+ ValueError: if password is longer than 72 bytes or if it contains the zero byte
+
+ """
+
+ password = tobytes(password, "utf-8")
+
+ if password.find(bchr(0)[0]) != -1:
+ raise ValueError("The password contains the zero byte")
+
+ if len(password) < 72:
+ password += b"\x00"
+
+ if salt is None:
+ salt = get_random_bytes(16)
+ if len(salt) != 16:
+ raise ValueError("bcrypt salt must be 16 bytes long")
+
+ ctext = _bcrypt_hash(password, cost, salt, b"OrpheanBeholderScryDoubt", True)
+
+ cost_enc = b"$" + bstr(str(cost).zfill(2))
+ salt_enc = b"$" + _bcrypt_encode(salt)
+ hash_enc = _bcrypt_encode(ctext[:-1]) # only use 23 bytes, not 24
+ return b"$2a" + cost_enc + salt_enc + hash_enc
+
+
+def bcrypt_check(password, bcrypt_hash):
+ """Verify if the provided password matches the given bcrypt hash.
+
+ Args:
+ password (byte string or string):
+ The secret password or pass phrase to test.
+ It must be at most 72 bytes long.
+ It must not contain the zero byte.
+ Unicode strings will be encoded as UTF-8.
+ bcrypt_hash (byte string, bytearray):
+ The reference bcrypt hash the password needs to be checked against.
+
+ Raises:
+ ValueError: if the password does not match
+ """
+
+ bcrypt_hash = tobytes(bcrypt_hash)
+
+ if len(bcrypt_hash) != 60:
+ raise ValueError("Incorrect length of the bcrypt hash: %d bytes instead of 60" % len(bcrypt_hash))
+
+ if bcrypt_hash[:4] != b'$2a$':
+ raise ValueError("Unsupported prefix")
+
+ p = re.compile(br'\$2a\$([0-9][0-9])\$([A-Za-z0-9./]{22,22})([A-Za-z0-9./]{31,31})')
+ r = p.match(bcrypt_hash)
+ if not r:
+ raise ValueError("Incorrect bcrypt hash format")
+
+ cost = int(r.group(1))
+ if not (4 <= cost <= 31):
+ raise ValueError("Incorrect cost")
+
+ salt = _bcrypt_decode(r.group(2))
+
+ bcrypt_hash2 = bcrypt(password, cost, salt)
+
+ secret = get_random_bytes(16)
+
+ mac1 = BLAKE2s.new(digest_bits=160, key=secret, data=bcrypt_hash).digest()
+ mac2 = BLAKE2s.new(digest_bits=160, key=secret, data=bcrypt_hash2).digest()
+ if mac1 != mac2:
+ raise ValueError("Incorrect bcrypt hash")
+
+
+def SP800_108_Counter(master, key_len, prf, num_keys=None, label=b'', context=b''):
+ """Derive one or more keys from a master secret using
+ a pseudorandom function in Counter Mode, as specified in
+ `NIST SP 800-108r1 `_.
+
+ Args:
+ master (byte string):
+ The secret value used by the KDF to derive the other keys.
+ It must not be a password.
+ The length on the secret must be consistent with the input expected by
+ the :data:`prf` function.
+ key_len (integer):
+ The length in bytes of each derived key.
+ prf (function):
+ A pseudorandom function that takes two byte strings as parameters:
+ the secret and an input. It returns another byte string.
+ num_keys (integer):
+ The number of keys to derive. Every key is :data:`key_len` bytes long.
+ By default, only 1 key is derived.
+ label (byte string):
+ Optional description of the purpose of the derived keys.
+ It must not contain zero bytes.
+ context (byte string):
+ Optional information pertaining to
+ the protocol that uses the keys, such as the identity of the
+ participants, nonces, session IDs, etc.
+ It must not contain zero bytes.
+
+ Return:
+ - a byte string (if ``num_keys`` is not specified), or
+ - a tuple of byte strings (if ``num_key`` is specified).
+ """
+
+ if num_keys is None:
+ num_keys = 1
+
+ if context.find(b'\x00') != -1:
+ raise ValueError("Null byte found in context")
+
+ key_len_enc = long_to_bytes(key_len * num_keys * 8, 4)
+ output_len = key_len * num_keys
+
+ i = 1
+ dk = b""
+ while len(dk) < output_len:
+ info = long_to_bytes(i, 4) + label + b'\x00' + context + key_len_enc
+ dk += prf(master, info)
+ i += 1
+ if i > 0xFFFFFFFF:
+ raise ValueError("Overflow in SP800 108 counter")
+
+ if num_keys == 1:
+ return dk[:key_len]
+ else:
+ kol = [dk[idx:idx + key_len]
+ for idx in iter_range(0, output_len, key_len)]
+ return kol
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/KDF.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/KDF.pyi
new file mode 100644
index 00000000..80691e0f
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/KDF.pyi
@@ -0,0 +1,44 @@
+from types import ModuleType
+from typing import Optional, Callable, Tuple, Union, Dict, Any, overload
+from typing_extensions import Literal
+
+Buffer=bytes|bytearray|memoryview
+
+RNG = Callable[[int], bytes]
+PRF = Callable[[bytes, bytes], bytes]
+
+def PBKDF1(password: str, salt: bytes, dkLen: int, count: Optional[int]=1000, hashAlgo: Optional[ModuleType]=None) -> bytes: ...
+def PBKDF2(password: str, salt: bytes, dkLen: Optional[int]=16, count: Optional[int]=1000, prf: Optional[RNG]=None, hmac_hash_module: Optional[ModuleType]=None) -> bytes: ...
+
+class _S2V(object):
+ def __init__(self, key: bytes, ciphermod: ModuleType, cipher_params: Optional[Dict[Any, Any]]=None) -> None: ...
+
+ @staticmethod
+ def new(key: bytes, ciphermod: ModuleType) -> None: ...
+ def update(self, item: bytes) -> None: ...
+ def derive(self) -> bytes: ...
+
+def _HKDF_extract(salt: Buffer, ikm: Buffer, hashmod: ModuleType) -> bytes: ...
+def _HKDF_expand(prk: Buffer, info: Buffer, L: int, hashmod) -> bytes : ...
+def HKDF(master: bytes, key_len: int, salt: bytes, hashmod: ModuleType, num_keys: Optional[int]=1, context: Optional[bytes]=None) -> Union[bytes, Tuple[bytes, ...]]: ...
+
+def scrypt(password: str, salt: str, key_len: int, N: int, r: int, p: int, num_keys: Optional[int]=1) -> Union[bytes, Tuple[bytes, ...]]: ...
+
+def _bcrypt_decode(data: bytes) -> bytes: ...
+def _bcrypt_hash(password:bytes , cost: int, salt: bytes, constant:bytes, invert:bool) -> bytes: ...
+def bcrypt(password: Union[bytes, str], cost: int, salt: Optional[bytes]=None) -> bytes: ...
+def bcrypt_check(password: Union[bytes, str], bcrypt_hash: Union[bytes, bytearray, str]) -> None: ...
+
+@overload
+def SP800_108_Counter(master: Buffer,
+ key_len: int,
+ prf: PRF,
+ num_keys: Literal[None] = None,
+ label: Buffer = b'', context: Buffer = b'') -> bytes: ...
+
+@overload
+def SP800_108_Counter(master: Buffer,
+ key_len: int,
+ prf: PRF,
+ num_keys: int,
+ label: Buffer = b'', context: Buffer = b'') -> Tuple[bytes]: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/SecretSharing.py b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/SecretSharing.py
new file mode 100644
index 00000000..8078c0ad
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/SecretSharing.py
@@ -0,0 +1,297 @@
+#
+# SecretSharing.py : distribute a secret amongst a group of participants
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.Util.py3compat import is_native_int
+from Crypto.Util import number
+from Crypto.Util.number import long_to_bytes, bytes_to_long
+from Crypto.Random import get_random_bytes as rng
+
+
+def _mult_gf2(f1, f2):
+ """Multiply two polynomials in GF(2)"""
+
+ # Ensure f2 is the smallest
+ if f2 > f1:
+ f1, f2 = f2, f1
+ z = 0
+ while f2:
+ if f2 & 1:
+ z ^= f1
+ f1 <<= 1
+ f2 >>= 1
+ return z
+
+
+def _div_gf2(a, b):
+ """
+ Compute division of polynomials over GF(2).
+ Given a and b, it finds two polynomials q and r such that:
+
+ a = b*q + r with deg(r)= d:
+ s = 1 << (deg(r) - d)
+ q ^= s
+ r ^= _mult_gf2(b, s)
+ return (q, r)
+
+
+class _Element(object):
+ """Element of GF(2^128) field"""
+
+ # The irreducible polynomial defining
+ # this field is 1 + x + x^2 + x^7 + x^128
+ irr_poly = 1 + 2 + 4 + 128 + 2 ** 128
+
+ def __init__(self, encoded_value):
+ """Initialize the element to a certain value.
+
+ The value passed as parameter is internally encoded as
+ a 128-bit integer, where each bit represents a polynomial
+ coefficient. The LSB is the constant coefficient.
+ """
+
+ if is_native_int(encoded_value):
+ self._value = encoded_value
+ elif len(encoded_value) == 16:
+ self._value = bytes_to_long(encoded_value)
+ else:
+ raise ValueError("The encoded value must be an integer or a 16 byte string")
+
+ def __eq__(self, other):
+ return self._value == other._value
+
+ def __int__(self):
+ """Return the field element, encoded as a 128-bit integer."""
+ return self._value
+
+ def encode(self):
+ """Return the field element, encoded as a 16 byte string."""
+ return long_to_bytes(self._value, 16)
+
+ def __mul__(self, factor):
+
+ f1 = self._value
+ f2 = factor._value
+
+ # Make sure that f2 is the smallest, to speed up the loop
+ if f2 > f1:
+ f1, f2 = f2, f1
+
+ if self.irr_poly in (f1, f2):
+ return _Element(0)
+
+ mask1 = 2 ** 128
+ v, z = f1, 0
+ while f2:
+ # if f2 ^ 1: z ^= v
+ mask2 = int(bin(f2 & 1)[2:] * 128, base=2)
+ z = (mask2 & (z ^ v)) | ((mask1 - mask2 - 1) & z)
+ v <<= 1
+ # if v & mask1: v ^= self.irr_poly
+ mask3 = int(bin((v >> 128) & 1)[2:] * 128, base=2)
+ v = (mask3 & (v ^ self.irr_poly)) | ((mask1 - mask3 - 1) & v)
+ f2 >>= 1
+ return _Element(z)
+
+ def __add__(self, term):
+ return _Element(self._value ^ term._value)
+
+ def inverse(self):
+ """Return the inverse of this element in GF(2^128)."""
+
+ # We use the Extended GCD algorithm
+ # http://en.wikipedia.org/wiki/Polynomial_greatest_common_divisor
+
+ if self._value == 0:
+ raise ValueError("Inversion of zero")
+
+ r0, r1 = self._value, self.irr_poly
+ s0, s1 = 1, 0
+ while r1 > 0:
+ q = _div_gf2(r0, r1)[0]
+ r0, r1 = r1, r0 ^ _mult_gf2(q, r1)
+ s0, s1 = s1, s0 ^ _mult_gf2(q, s1)
+ return _Element(s0)
+
+ def __pow__(self, exponent):
+ result = _Element(self._value)
+ for _ in range(exponent - 1):
+ result = result * self
+ return result
+
+
+class Shamir(object):
+ """Shamir's secret sharing scheme.
+
+ A secret is split into ``n`` shares, and it is sufficient to collect
+ ``k`` of them to reconstruct the secret.
+ """
+
+ @staticmethod
+ def split(k, n, secret, ssss=False):
+ """Split a secret into ``n`` shares.
+
+ The secret can be reconstructed later using just ``k`` shares
+ out of the original ``n``.
+ Each share must be kept confidential to the person it was
+ assigned to.
+
+ Each share is associated to an index (starting from 1).
+
+ Args:
+ k (integer):
+ The number of shares needed to reconstruct the secret.
+ n (integer):
+ The number of shares to create (at least ``k``).
+ secret (byte string):
+ A byte string of 16 bytes (e.g. an AES 128 key).
+ ssss (bool):
+ If ``True``, the shares can be used with the ``ssss`` utility
+ (without using the "diffusion layer").
+ Default: ``False``.
+
+ Return (tuples):
+ ``n`` tuples, one per participant.
+ A tuple contains two items:
+
+ 1. the unique index (an integer)
+ 2. the share (16 bytes)
+ """
+
+ #
+ # We create a polynomial with random coefficients in GF(2^128):
+ #
+ # p(x) = c_0 + \sum_{i=1}^{k-1} c_i * x^i
+ #
+ # c_0 is the secret.
+ #
+
+ coeffs = [_Element(rng(16)) for i in range(k - 1)]
+ coeffs.append(_Element(secret))
+
+ # Each share is y_i = p(x_i) where x_i
+ # is the index assigned to the share.
+
+ def make_share(user, coeffs, ssss):
+ idx = _Element(user)
+
+ # Horner's method
+ share = _Element(0)
+ for coeff in coeffs:
+ share = idx * share + coeff
+
+ # The ssss utility actually uses:
+ #
+ # p(x) = c_0 + \sum_{i=1}^{k-1} c_i * x^i + x^k
+ #
+ if ssss:
+ share += _Element(user) ** len(coeffs)
+
+ return share.encode()
+
+ return [(i, make_share(i, coeffs, ssss)) for i in range(1, n + 1)]
+
+ @staticmethod
+ def combine(shares, ssss=False):
+ """Recombine a secret, if enough shares are presented.
+
+ Args:
+ shares (tuples):
+ The *k* tuples, each containing the index (an integer) and
+ the share (a byte string, 16 bytes long) that were assigned to
+ a participant.
+
+ .. note::
+
+ Pass exactly as many share as they are required,
+ and no more.
+
+ ssss (bool):
+ If ``True``, the shares were produced by the ``ssss`` utility
+ (without using the "diffusion layer").
+ Default: ``False``.
+
+ Return:
+ The original secret, as a byte string (16 bytes long).
+ """
+
+ #
+ # Given k points (x,y), the interpolation polynomial of degree k-1 is:
+ #
+ # L(x) = \sum_{j=0}^{k-1} y_i * l_j(x)
+ #
+ # where:
+ #
+ # l_j(x) = \prod_{ \overset{0 \le m \le k-1}{m \ne j} }
+ # \frac{x - x_m}{x_j - x_m}
+ #
+ # However, in this case we are purely interested in the constant
+ # coefficient of L(x).
+ #
+
+ k = len(shares)
+
+ gf_shares = []
+ for x in shares:
+ idx = _Element(x[0])
+ value = _Element(x[1])
+ if any(y[0] == idx for y in gf_shares):
+ raise ValueError("Duplicate share")
+ if ssss:
+ value += idx ** k
+ gf_shares.append((idx, value))
+
+ result = _Element(0)
+ for j in range(k):
+ x_j, y_j = gf_shares[j]
+
+ numerator = _Element(1)
+ denominator = _Element(1)
+
+ for m in range(k):
+ x_m = gf_shares[m][0]
+ if m != j:
+ numerator *= x_m
+ denominator *= x_j + x_m
+ result += y_j * numerator * denominator.inverse()
+
+ return result.encode()
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/SecretSharing.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/SecretSharing.pyi
new file mode 100644
index 00000000..5952c993
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/SecretSharing.pyi
@@ -0,0 +1,22 @@
+from typing import Union, List, Tuple, Optional
+
+def _mult_gf2(f1: int, f2: int) -> int : ...
+def _div_gf2(a: int, b: int) -> int : ...
+
+class _Element(object):
+ irr_poly: int
+ def __init__(self, encoded_value: Union[int, bytes]) -> None: ...
+ def __eq__(self, other) -> bool: ...
+ def __int__(self) -> int: ...
+ def encode(self) -> bytes: ...
+ def __mul__(self, factor: int) -> _Element: ...
+ def __add__(self, term: _Element) -> _Element: ...
+ def inverse(self) -> _Element: ...
+ def __pow__(self, exponent) -> _Element: ...
+
+class Shamir(object):
+ @staticmethod
+ def split(k: int, n: int, secret: bytes, ssss: Optional[bool]) -> List[Tuple[int, bytes]]: ...
+ @staticmethod
+ def combine(shares: List[Tuple[int, bytes]], ssss: Optional[bool]) -> bytes: ...
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/__init__.py
new file mode 100644
index 00000000..76e22bf6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/__init__.py
@@ -0,0 +1,31 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+__all__ = ['KDF', 'SecretSharing', 'DH']
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/__init__.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/__init__.pyi
new file mode 100644
index 00000000..377ed901
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/__init__.pyi
@@ -0,0 +1 @@
+__all__ = ['KDF.pyi', 'SecretSharing.pyi']
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/_scrypt.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/_scrypt.abi3.so
new file mode 100755
index 00000000..db66506d
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Protocol/_scrypt.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/DSA.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/DSA.py
new file mode 100644
index 00000000..4c7f47b6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/DSA.py
@@ -0,0 +1,682 @@
+# -*- coding: utf-8 -*-
+#
+# PublicKey/DSA.py : DSA signature primitive
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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__ = ['generate', 'construct', 'DsaKey', 'import_key' ]
+
+import binascii
+import struct
+import itertools
+
+from Crypto.Util.py3compat import bchr, bord, tobytes, tostr, iter_range
+
+from Crypto import Random
+from Crypto.IO import PKCS8, PEM
+from Crypto.Hash import SHA256
+from Crypto.Util.asn1 import (
+ DerObject, DerSequence,
+ DerInteger, DerObjectId,
+ DerBitString,
+ )
+
+from Crypto.Math.Numbers import Integer
+from Crypto.Math.Primality import (test_probable_prime, COMPOSITE,
+ PROBABLY_PRIME)
+
+from Crypto.PublicKey import (_expand_subject_public_key_info,
+ _create_subject_public_key_info,
+ _extract_subject_public_key_info)
+
+# ; The following ASN.1 types are relevant for DSA
+#
+# SubjectPublicKeyInfo ::= SEQUENCE {
+# algorithm AlgorithmIdentifier,
+# subjectPublicKey BIT STRING
+# }
+#
+# id-dsa ID ::= { iso(1) member-body(2) us(840) x9-57(10040) x9cm(4) 1 }
+#
+# ; See RFC3279
+# Dss-Parms ::= SEQUENCE {
+# p INTEGER,
+# q INTEGER,
+# g INTEGER
+# }
+#
+# DSAPublicKey ::= INTEGER
+#
+# DSSPrivatKey_OpenSSL ::= SEQUENCE
+# version INTEGER,
+# p INTEGER,
+# q INTEGER,
+# g INTEGER,
+# y INTEGER,
+# x INTEGER
+# }
+#
+
+class DsaKey(object):
+ r"""Class defining an actual DSA key.
+ Do not instantiate directly.
+ Use :func:`generate`, :func:`construct` or :func:`import_key` instead.
+
+ :ivar p: DSA modulus
+ :vartype p: integer
+
+ :ivar q: Order of the subgroup
+ :vartype q: integer
+
+ :ivar g: Generator
+ :vartype g: integer
+
+ :ivar y: Public key
+ :vartype y: integer
+
+ :ivar x: Private key
+ :vartype x: integer
+
+ :undocumented: exportKey, publickey
+ """
+
+ _keydata = ['y', 'g', 'p', 'q', 'x']
+
+ def __init__(self, key_dict):
+ input_set = set(key_dict.keys())
+ public_set = set(('y' , 'g', 'p', 'q'))
+ if not public_set.issubset(input_set):
+ raise ValueError("Some DSA components are missing = %s" %
+ str(public_set - input_set))
+ extra_set = input_set - public_set
+ if extra_set and extra_set != set(('x',)):
+ raise ValueError("Unknown DSA components = %s" %
+ str(extra_set - set(('x',))))
+ self._key = dict(key_dict)
+
+ def _sign(self, m, k):
+ if not self.has_private():
+ raise TypeError("DSA public key cannot be used for signing")
+ if not (1 < k < self.q):
+ raise ValueError("k is not between 2 and q-1")
+
+ x, q, p, g = [self._key[comp] for comp in ['x', 'q', 'p', 'g']]
+
+ blind_factor = Integer.random_range(min_inclusive=1,
+ max_exclusive=q)
+ inv_blind_k = (blind_factor * k).inverse(q)
+ blind_x = x * blind_factor
+
+ r = pow(g, k, p) % q # r = (g**k mod p) mod q
+ s = (inv_blind_k * (blind_factor * m + blind_x * r)) % q
+ return map(int, (r, s))
+
+ def _verify(self, m, sig):
+ r, s = sig
+ y, q, p, g = [self._key[comp] for comp in ['y', 'q', 'p', 'g']]
+ if not (0 < r < q) or not (0 < s < q):
+ return False
+ w = Integer(s).inverse(q)
+ u1 = (w * m) % q
+ u2 = (w * r) % q
+ v = (pow(g, u1, p) * pow(y, u2, p) % p) % q
+ return v == r
+
+ def has_private(self):
+ """Whether this is a DSA private key"""
+
+ return 'x' in self._key
+
+ def can_encrypt(self): # legacy
+ return False
+
+ def can_sign(self): # legacy
+ return True
+
+ def public_key(self):
+ """A matching DSA public key.
+
+ Returns:
+ a new :class:`DsaKey` object
+ """
+
+ public_components = dict((k, self._key[k]) for k in ('y', 'g', 'p', 'q'))
+ return DsaKey(public_components)
+
+ def __eq__(self, other):
+ if bool(self.has_private()) != bool(other.has_private()):
+ return False
+
+ result = True
+ for comp in self._keydata:
+ result = result and (getattr(self._key, comp, None) ==
+ getattr(other._key, comp, None))
+ return result
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __getstate__(self):
+ # DSA key is not pickable
+ from pickle import PicklingError
+ raise PicklingError
+
+ def domain(self):
+ """The DSA domain parameters.
+
+ Returns
+ tuple : (p,q,g)
+ """
+
+ return [int(self._key[comp]) for comp in ('p', 'q', 'g')]
+
+ def __repr__(self):
+ attrs = []
+ for k in self._keydata:
+ if k == 'p':
+ bits = Integer(self.p).size_in_bits()
+ attrs.append("p(%d)" % (bits,))
+ elif hasattr(self, k):
+ attrs.append(k)
+ if self.has_private():
+ attrs.append("private")
+ # PY3K: This is meant to be text, do not change to bytes (data)
+ return "<%s @0x%x %s>" % (self.__class__.__name__, id(self), ",".join(attrs))
+
+ def __getattr__(self, item):
+ try:
+ return int(self._key[item])
+ except KeyError:
+ raise AttributeError(item)
+
+ def export_key(self, format='PEM', pkcs8=None, passphrase=None,
+ protection=None, randfunc=None):
+ """Export this DSA key.
+
+ Args:
+ format (string):
+ The encoding for the output:
+
+ - *'PEM'* (default). ASCII as per `RFC1421`_/ `RFC1423`_.
+ - *'DER'*. Binary ASN.1 encoding.
+ - *'OpenSSH'*. ASCII one-liner as per `RFC4253`_.
+ Only suitable for public keys, not for private keys.
+
+ passphrase (string):
+ *Private keys only*. The pass phrase to protect the output.
+
+ pkcs8 (boolean):
+ *Private keys only*. If ``True`` (default), the key is encoded
+ with `PKCS#8`_. If ``False``, it is encoded in the custom
+ OpenSSL/OpenSSH container.
+
+ protection (string):
+ *Only in combination with a pass phrase*.
+ The encryption scheme to use to protect the output.
+
+ If :data:`pkcs8` takes value ``True``, this is the PKCS#8
+ algorithm to use for deriving the secret and encrypting
+ the private DSA key.
+ For a complete list of algorithms, see :mod:`Crypto.IO.PKCS8`.
+ The default is *PBKDF2WithHMAC-SHA1AndDES-EDE3-CBC*.
+
+ If :data:`pkcs8` is ``False``, the obsolete PEM encryption scheme is
+ used. It is based on MD5 for key derivation, and Triple DES for
+ encryption. Parameter :data:`protection` is then ignored.
+
+ The combination ``format='DER'`` and ``pkcs8=False`` is not allowed
+ if a passphrase is present.
+
+ randfunc (callable):
+ A function that returns random bytes.
+ By default it is :func:`Crypto.Random.get_random_bytes`.
+
+ Returns:
+ byte string : the encoded key
+
+ Raises:
+ ValueError : when the format is unknown or when you try to encrypt a private
+ key with *DER* format and OpenSSL/OpenSSH.
+
+ .. warning::
+ If you don't provide a pass phrase, the private key will be
+ exported in the clear!
+
+ .. _RFC1421: http://www.ietf.org/rfc/rfc1421.txt
+ .. _RFC1423: http://www.ietf.org/rfc/rfc1423.txt
+ .. _RFC4253: http://www.ietf.org/rfc/rfc4253.txt
+ .. _`PKCS#8`: http://www.ietf.org/rfc/rfc5208.txt
+ """
+
+ if passphrase is not None:
+ passphrase = tobytes(passphrase)
+
+ if randfunc is None:
+ randfunc = Random.get_random_bytes
+
+ if format == 'OpenSSH':
+ tup1 = [self._key[x].to_bytes() for x in ('p', 'q', 'g', 'y')]
+
+ def func(x):
+ if (bord(x[0]) & 0x80):
+ return bchr(0) + x
+ else:
+ return x
+
+ tup2 = [func(x) for x in tup1]
+ keyparts = [b'ssh-dss'] + tup2
+ keystring = b''.join(
+ [struct.pack(">I", len(kp)) + kp for kp in keyparts]
+ )
+ return b'ssh-dss ' + binascii.b2a_base64(keystring)[:-1]
+
+ # DER format is always used, even in case of PEM, which simply
+ # encodes it into BASE64.
+ params = DerSequence([self.p, self.q, self.g])
+ if self.has_private():
+ if pkcs8 is None:
+ pkcs8 = True
+ if pkcs8:
+ if not protection:
+ protection = 'PBKDF2WithHMAC-SHA1AndDES-EDE3-CBC'
+ private_key = DerInteger(self.x).encode()
+ binary_key = PKCS8.wrap(
+ private_key, oid, passphrase,
+ protection, key_params=params,
+ randfunc=randfunc
+ )
+ if passphrase:
+ key_type = 'ENCRYPTED PRIVATE'
+ else:
+ key_type = 'PRIVATE'
+ passphrase = None
+ else:
+ if format != 'PEM' and passphrase:
+ raise ValueError("DSA private key cannot be encrypted")
+ ints = [0, self.p, self.q, self.g, self.y, self.x]
+ binary_key = DerSequence(ints).encode()
+ key_type = "DSA PRIVATE"
+ else:
+ if pkcs8:
+ raise ValueError("PKCS#8 is only meaningful for private keys")
+
+ binary_key = _create_subject_public_key_info(oid,
+ DerInteger(self.y), params)
+ key_type = "PUBLIC"
+
+ if format == 'DER':
+ return binary_key
+ if format == 'PEM':
+ pem_str = PEM.encode(
+ binary_key, key_type + " KEY",
+ passphrase, randfunc
+ )
+ return tobytes(pem_str)
+ raise ValueError("Unknown key format '%s'. Cannot export the DSA key." % format)
+
+ # Backward-compatibility
+ exportKey = export_key
+ publickey = public_key
+
+ # Methods defined in PyCrypto that we don't support anymore
+
+ def sign(self, M, K):
+ raise NotImplementedError("Use module Crypto.Signature.DSS instead")
+
+ def verify(self, M, signature):
+ raise NotImplementedError("Use module Crypto.Signature.DSS instead")
+
+ def encrypt(self, plaintext, K):
+ raise NotImplementedError
+
+ def decrypt(self, ciphertext):
+ raise NotImplementedError
+
+ def blind(self, M, B):
+ raise NotImplementedError
+
+ def unblind(self, M, B):
+ raise NotImplementedError
+
+ def size(self):
+ raise NotImplementedError
+
+
+def _generate_domain(L, randfunc):
+ """Generate a new set of DSA domain parameters"""
+
+ N = { 1024:160, 2048:224, 3072:256 }.get(L)
+ if N is None:
+ raise ValueError("Invalid modulus length (%d)" % L)
+
+ outlen = SHA256.digest_size * 8
+ n = (L + outlen - 1) // outlen - 1 # ceil(L/outlen) -1
+ b_ = L - 1 - (n * outlen)
+
+ # Generate q (A.1.1.2)
+ q = Integer(4)
+ upper_bit = 1 << (N - 1)
+ while test_probable_prime(q, randfunc) != PROBABLY_PRIME:
+ seed = randfunc(64)
+ U = Integer.from_bytes(SHA256.new(seed).digest()) & (upper_bit - 1)
+ q = U | upper_bit | 1
+
+ assert(q.size_in_bits() == N)
+
+ # Generate p (A.1.1.2)
+ offset = 1
+ upper_bit = 1 << (L - 1)
+ while True:
+ V = [ SHA256.new(seed + Integer(offset + j).to_bytes()).digest()
+ for j in iter_range(n + 1) ]
+ V = [ Integer.from_bytes(v) for v in V ]
+ W = sum([V[i] * (1 << (i * outlen)) for i in iter_range(n)],
+ (V[n] & ((1 << b_) - 1)) * (1 << (n * outlen)))
+
+ X = Integer(W + upper_bit) # 2^{L-1} < X < 2^{L}
+ assert(X.size_in_bits() == L)
+
+ c = X % (q * 2)
+ p = X - (c - 1) # 2q divides (p-1)
+ if p.size_in_bits() == L and \
+ test_probable_prime(p, randfunc) == PROBABLY_PRIME:
+ break
+ offset += n + 1
+
+ # Generate g (A.2.3, index=1)
+ e = (p - 1) // q
+ for count in itertools.count(1):
+ U = seed + b"ggen" + bchr(1) + Integer(count).to_bytes()
+ W = Integer.from_bytes(SHA256.new(U).digest())
+ g = pow(W, e, p)
+ if g != 1:
+ break
+
+ return (p, q, g, seed)
+
+
+def generate(bits, randfunc=None, domain=None):
+ """Generate a new DSA key pair.
+
+ The algorithm follows Appendix A.1/A.2 and B.1 of `FIPS 186-4`_,
+ respectively for domain generation and key pair generation.
+
+ Args:
+ bits (integer):
+ Key length, or size (in bits) of the DSA modulus *p*.
+ It must be 1024, 2048 or 3072.
+
+ randfunc (callable):
+ Random number generation function; it accepts a single integer N
+ and return a string of random data N bytes long.
+ If not specified, :func:`Crypto.Random.get_random_bytes` is used.
+
+ domain (tuple):
+ The DSA domain parameters *p*, *q* and *g* as a list of 3
+ integers. Size of *p* and *q* must comply to `FIPS 186-4`_.
+ If not specified, the parameters are created anew.
+
+ Returns:
+ :class:`DsaKey` : a new DSA key object
+
+ Raises:
+ ValueError : when **bits** is too little, too big, or not a multiple of 64.
+
+ .. _FIPS 186-4: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
+ """
+
+ if randfunc is None:
+ randfunc = Random.get_random_bytes
+
+ if domain:
+ p, q, g = map(Integer, domain)
+
+ ## Perform consistency check on domain parameters
+ # P and Q must be prime
+ fmt_error = test_probable_prime(p) == COMPOSITE
+ fmt_error |= test_probable_prime(q) == COMPOSITE
+ # Verify Lagrange's theorem for sub-group
+ fmt_error |= ((p - 1) % q) != 0
+ fmt_error |= g <= 1 or g >= p
+ fmt_error |= pow(g, q, p) != 1
+ if fmt_error:
+ raise ValueError("Invalid DSA domain parameters")
+ else:
+ p, q, g, _ = _generate_domain(bits, randfunc)
+
+ L = p.size_in_bits()
+ N = q.size_in_bits()
+
+ if L != bits:
+ raise ValueError("Mismatch between size of modulus (%d)"
+ " and 'bits' parameter (%d)" % (L, bits))
+
+ if (L, N) not in [(1024, 160), (2048, 224),
+ (2048, 256), (3072, 256)]:
+ raise ValueError("Lengths of p and q (%d, %d) are not compatible"
+ "to FIPS 186-3" % (L, N))
+
+ if not 1 < g < p:
+ raise ValueError("Incorrent DSA generator")
+
+ # B.1.1
+ c = Integer.random(exact_bits=N + 64, randfunc=randfunc)
+ x = c % (q - 1) + 1 # 1 <= x <= q-1
+ y = pow(g, x, p)
+
+ key_dict = { 'y':y, 'g':g, 'p':p, 'q':q, 'x':x }
+ return DsaKey(key_dict)
+
+
+def construct(tup, consistency_check=True):
+ """Construct a DSA key from a tuple of valid DSA components.
+
+ Args:
+ tup (tuple):
+ A tuple of long integers, with 4 or 5 items
+ in the following order:
+
+ 1. Public key (*y*).
+ 2. Sub-group generator (*g*).
+ 3. Modulus, finite field order (*p*).
+ 4. Sub-group order (*q*).
+ 5. Private key (*x*). Optional.
+
+ consistency_check (boolean):
+ If ``True``, the library will verify that the provided components
+ fulfil the main DSA properties.
+
+ Raises:
+ ValueError: when the key being imported fails the most basic DSA validity checks.
+
+ Returns:
+ :class:`DsaKey` : a DSA key object
+ """
+
+ key_dict = dict(zip(('y', 'g', 'p', 'q', 'x'), map(Integer, tup)))
+ key = DsaKey(key_dict)
+
+ fmt_error = False
+ if consistency_check:
+ # P and Q must be prime
+ fmt_error = test_probable_prime(key.p) == COMPOSITE
+ fmt_error |= test_probable_prime(key.q) == COMPOSITE
+ # Verify Lagrange's theorem for sub-group
+ fmt_error |= ((key.p - 1) % key.q) != 0
+ fmt_error |= key.g <= 1 or key.g >= key.p
+ fmt_error |= pow(key.g, key.q, key.p) != 1
+ # Public key
+ fmt_error |= key.y <= 0 or key.y >= key.p
+ if hasattr(key, 'x'):
+ fmt_error |= key.x <= 0 or key.x >= key.q
+ fmt_error |= pow(key.g, key.x, key.p) != key.y
+
+ if fmt_error:
+ raise ValueError("Invalid DSA key components")
+
+ return key
+
+
+# Dss-Parms ::= SEQUENCE {
+# p OCTET STRING,
+# q OCTET STRING,
+# g OCTET STRING
+# }
+# DSAPublicKey ::= INTEGER -- public key, y
+
+def _import_openssl_private(encoded, passphrase, params):
+ if params:
+ raise ValueError("DSA private key already comes with parameters")
+ der = DerSequence().decode(encoded, nr_elements=6, only_ints_expected=True)
+ if der[0] != 0:
+ raise ValueError("No version found")
+ tup = [der[comp] for comp in (4, 3, 1, 2, 5)]
+ return construct(tup)
+
+
+def _import_subjectPublicKeyInfo(encoded, passphrase, params):
+
+ algoid, encoded_key, emb_params = _expand_subject_public_key_info(encoded)
+ if algoid != oid:
+ raise ValueError("No DSA subjectPublicKeyInfo")
+ if params and emb_params:
+ raise ValueError("Too many DSA parameters")
+
+ y = DerInteger().decode(encoded_key).value
+ p, q, g = list(DerSequence().decode(params or emb_params))
+ tup = (y, g, p, q)
+ return construct(tup)
+
+
+def _import_x509_cert(encoded, passphrase, params):
+
+ sp_info = _extract_subject_public_key_info(encoded)
+ return _import_subjectPublicKeyInfo(sp_info, None, params)
+
+
+def _import_pkcs8(encoded, passphrase, params):
+ if params:
+ raise ValueError("PKCS#8 already includes parameters")
+ k = PKCS8.unwrap(encoded, passphrase)
+ if k[0] != oid:
+ raise ValueError("No PKCS#8 encoded DSA key")
+ x = DerInteger().decode(k[1]).value
+ p, q, g = list(DerSequence().decode(k[2]))
+ tup = (pow(g, x, p), g, p, q, x)
+ return construct(tup)
+
+
+def _import_key_der(key_data, passphrase, params):
+ """Import a DSA key (public or private half), encoded in DER form."""
+
+ decodings = (_import_openssl_private,
+ _import_subjectPublicKeyInfo,
+ _import_x509_cert,
+ _import_pkcs8)
+
+ for decoding in decodings:
+ try:
+ return decoding(key_data, passphrase, params)
+ except ValueError:
+ pass
+
+ raise ValueError("DSA key format is not supported")
+
+
+def import_key(extern_key, passphrase=None):
+ """Import a DSA key.
+
+ Args:
+ extern_key (string or byte string):
+ The DSA key to import.
+
+ The following formats are supported for a DSA **public** key:
+
+ - X.509 certificate (binary DER or PEM)
+ - X.509 ``subjectPublicKeyInfo`` (binary DER or PEM)
+ - OpenSSH (ASCII one-liner, see `RFC4253`_)
+
+ The following formats are supported for a DSA **private** key:
+
+ - `PKCS#8`_ ``PrivateKeyInfo`` or ``EncryptedPrivateKeyInfo``
+ DER SEQUENCE (binary or PEM)
+ - OpenSSL/OpenSSH custom format (binary or PEM)
+
+ For details about the PEM encoding, see `RFC1421`_/`RFC1423`_.
+
+ passphrase (string):
+ In case of an encrypted private key, this is the pass phrase
+ from which the decryption key is derived.
+
+ Encryption may be applied either at the `PKCS#8`_ or at the PEM level.
+
+ Returns:
+ :class:`DsaKey` : a DSA key object
+
+ Raises:
+ ValueError : when the given key cannot be parsed (possibly because
+ the pass phrase is wrong).
+
+ .. _RFC1421: http://www.ietf.org/rfc/rfc1421.txt
+ .. _RFC1423: http://www.ietf.org/rfc/rfc1423.txt
+ .. _RFC4253: http://www.ietf.org/rfc/rfc4253.txt
+ .. _PKCS#8: http://www.ietf.org/rfc/rfc5208.txt
+ """
+
+ extern_key = tobytes(extern_key)
+ if passphrase is not None:
+ passphrase = tobytes(passphrase)
+
+ if extern_key.startswith(b'-----'):
+ # This is probably a PEM encoded key
+ (der, marker, enc_flag) = PEM.decode(tostr(extern_key), passphrase)
+ if enc_flag:
+ passphrase = None
+ return _import_key_der(der, passphrase, None)
+
+ if extern_key.startswith(b'ssh-dss '):
+ # This is probably a public OpenSSH key
+ keystring = binascii.a2b_base64(extern_key.split(b' ')[1])
+ keyparts = []
+ while len(keystring) > 4:
+ length = struct.unpack(">I", keystring[:4])[0]
+ keyparts.append(keystring[4:4 + length])
+ keystring = keystring[4 + length:]
+ if keyparts[0] == b"ssh-dss":
+ tup = [Integer.from_bytes(keyparts[x]) for x in (4, 3, 1, 2)]
+ return construct(tup)
+
+ if len(extern_key) > 0 and bord(extern_key[0]) == 0x30:
+ # This is probably a DER encoded key
+ return _import_key_der(extern_key, passphrase, None)
+
+ raise ValueError("DSA key format is not supported")
+
+
+# Backward compatibility
+importKey = import_key
+
+#: `Object ID`_ for a DSA key.
+#:
+#: id-dsa ID ::= { iso(1) member-body(2) us(840) x9-57(10040) x9cm(4) 1 }
+#:
+#: .. _`Object ID`: http://www.alvestrand.no/objectid/1.2.840.10040.4.1.html
+oid = "1.2.840.10040.4.1"
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/DSA.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/DSA.pyi
new file mode 100644
index 00000000..354ac1f0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/DSA.pyi
@@ -0,0 +1,31 @@
+from typing import Dict, Tuple, Callable, Union, Optional
+
+__all__ = ['generate', 'construct', 'DsaKey', 'import_key' ]
+
+RNG = Callable[[int], bytes]
+
+class DsaKey(object):
+ def __init__(self, key_dict: Dict[str, int]) -> None: ...
+ def has_private(self) -> bool: ...
+ def can_encrypt(self) -> bool: ... # legacy
+ def can_sign(self) -> bool: ... # legacy
+ def public_key(self) -> DsaKey: ...
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
+ def __getstate__(self) -> None: ...
+ def domain(self) -> Tuple[int, int, int]: ...
+ def __repr__(self) -> str: ...
+ def __getattr__(self, item: str) -> int: ...
+ def export_key(self, format: Optional[str]="PEM", pkcs8: Optional[bool]=None, passphrase: Optional[str]=None,
+ protection: Optional[str]=None, randfunc: Optional[RNG]=None) -> bytes: ...
+ # Backward-compatibility
+ exportKey = export_key
+ publickey = public_key
+
+def generate(bits: int, randfunc: Optional[RNG]=None, domain: Optional[Tuple[int, int, int]]=None) -> DsaKey: ...
+def construct(tup: Union[Tuple[int, int, int, int], Tuple[int, int, int, int, int]], consistency_check: Optional[bool]=True) -> DsaKey: ...
+def import_key(extern_key: Union[str, bytes], passphrase: Optional[str]=None) -> DsaKey: ...
+# Backward compatibility
+importKey = import_key
+
+oid: str
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ECC.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ECC.py
new file mode 100644
index 00000000..46789f90
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ECC.py
@@ -0,0 +1,1342 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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 __future__ import print_function
+
+import re
+import struct
+import binascii
+
+from Crypto.Util.py3compat import bord, tobytes, tostr, bchr, is_string
+
+from Crypto.Math.Numbers import Integer
+from Crypto.Util.asn1 import (DerObjectId, DerOctetString, DerSequence,
+ DerBitString)
+
+from Crypto.PublicKey import (_expand_subject_public_key_info,
+ _create_subject_public_key_info,
+ _extract_subject_public_key_info)
+
+from Crypto.Hash import SHA512, SHAKE256
+
+from Crypto.Random import get_random_bytes
+
+from ._point import EccPoint, EccXPoint, _curves
+from ._point import CurveID as _CurveID
+
+
+class UnsupportedEccFeature(ValueError):
+ pass
+
+
+class EccKey(object):
+ r"""Class defining an ECC key.
+ Do not instantiate directly.
+ Use :func:`generate`, :func:`construct` or :func:`import_key` instead.
+
+ :ivar curve: The **canonical** name of the curve as defined in the `ECC table`_.
+ :vartype curve: string
+
+ :ivar pointQ: an ECC point representing the public component.
+ :vartype pointQ: :class:`EccPoint` or :class:`EccXPoint`
+
+ :ivar d: A scalar that represents the private component
+ in NIST P curves. It is smaller than the
+ order of the generator point.
+ :vartype d: integer
+
+ :ivar seed: A seed that represents the private component
+ in Ed22519 (32 bytes), Curve25519 (32 bytes),
+ Curve448 (56 bytes), Ed448 (57 bytes).
+ :vartype seed: bytes
+ """
+
+ def __init__(self, **kwargs):
+ """Create a new ECC key
+
+ Keywords:
+ curve : string
+ The name of the curve.
+ d : integer
+ Mandatory for a private key one NIST P curves.
+ It must be in the range ``[1..order-1]``.
+ seed : bytes
+ Mandatory for a private key on Ed25519 (32 bytes),
+ Curve25519 (32 bytes), Curve448 (56 bytes) or Ed448 (57 bytes).
+ point : EccPoint or EccXPoint
+ Mandatory for a public key. If provided for a private key,
+ the implementation will NOT check whether it matches ``d``.
+
+ Only one parameter among ``d``, ``seed`` or ``point`` may be used.
+ """
+
+ kwargs_ = dict(kwargs)
+ curve_name = kwargs_.pop("curve", None)
+ self._d = kwargs_.pop("d", None)
+ self._seed = kwargs_.pop("seed", None)
+ self._point = kwargs_.pop("point", None)
+ if curve_name is None and self._point:
+ curve_name = self._point.curve
+ if kwargs_:
+ raise TypeError("Unknown parameters: " + str(kwargs_))
+
+ if curve_name not in _curves:
+ raise ValueError("Unsupported curve (%s)" % curve_name)
+ self._curve = _curves[curve_name]
+ self.curve = self._curve.canonical
+
+ count = int(self._d is not None) + int(self._seed is not None)
+
+ if count == 0:
+ if self._point is None:
+ raise ValueError("At lest one between parameters 'point', 'd' or 'seed' must be specified")
+ return
+
+ if count == 2:
+ raise ValueError("Parameters d and seed are mutually exclusive")
+
+ # NIST P curves work with d, EdDSA works with seed
+
+ # RFC 8032, 5.1.5
+ if self._curve.id == _CurveID.ED25519:
+ if self._d is not None:
+ raise ValueError("Parameter d can only be used with NIST P curves")
+ if len(self._seed) != 32:
+ raise ValueError("Parameter seed must be 32 bytes long for Ed25519")
+ seed_hash = SHA512.new(self._seed).digest() # h
+ self._prefix = seed_hash[32:]
+ tmp = bytearray(seed_hash[:32])
+ tmp[0] &= 0xF8
+ tmp[31] = (tmp[31] & 0x7F) | 0x40
+ self._d = Integer.from_bytes(tmp, byteorder='little')
+ # RFC 8032, 5.2.5
+ elif self._curve.id == _CurveID.ED448:
+ if self._d is not None:
+ raise ValueError("Parameter d can only be used with NIST P curves")
+ if len(self._seed) != 57:
+ raise ValueError("Parameter seed must be 57 bytes long for Ed448")
+ seed_hash = SHAKE256.new(self._seed).read(114) # h
+ self._prefix = seed_hash[57:]
+ tmp = bytearray(seed_hash[:57])
+ tmp[0] &= 0xFC
+ tmp[55] |= 0x80
+ tmp[56] = 0
+ self._d = Integer.from_bytes(tmp, byteorder='little')
+ # RFC 7748, 5
+ elif self._curve.id == _CurveID.CURVE25519:
+ if self._d is not None:
+ raise ValueError("Parameter d can only be used with NIST P curves")
+ if len(self._seed) != 32:
+ raise ValueError("Parameter seed must be 32 bytes long for Curve25519")
+ tmp = bytearray(self._seed)
+ tmp[0] &= 0xF8
+ tmp[31] = (tmp[31] & 0x7F) | 0x40
+ self._d = Integer.from_bytes(tmp, byteorder='little')
+ elif self._curve.id == _CurveID.CURVE448:
+ if self._d is not None:
+ raise ValueError("Parameter d can only be used with NIST P curves")
+ if len(self._seed) != 56:
+ raise ValueError("Parameter seed must be 56 bytes long for Curve448")
+ tmp = bytearray(self._seed)
+ tmp[0] &= 0xFC
+ tmp[55] |= 0x80
+ self._d = Integer.from_bytes(tmp, byteorder='little')
+
+ else:
+ if self._seed is not None:
+ raise ValueError("Parameter 'seed' cannot be used with NIST P-curves")
+ self._d = Integer(self._d)
+ if not 1 <= self._d < self._curve.order:
+ raise ValueError("Parameter d must be an integer smaller than the curve order")
+
+ def __eq__(self, other):
+ if not isinstance(other, EccKey):
+ return False
+
+ if other.has_private() != self.has_private():
+ return False
+
+ return other.pointQ == self.pointQ
+
+ def __repr__(self):
+ if self.has_private():
+ if self._curve.is_edwards:
+ extra = ", seed=%s" % tostr(binascii.hexlify(self._seed))
+ else:
+ extra = ", d=%d" % int(self._d)
+ else:
+ extra = ""
+ if self._curve.id in (_CurveID.CURVE25519,
+ _CurveID.CURVE448):
+ x = self.pointQ.x
+ result = "EccKey(curve='%s', point_x=%d%s)" % (self._curve.canonical, x, extra)
+ else:
+ x, y = self.pointQ.xy
+ result = "EccKey(curve='%s', point_x=%d, point_y=%d%s)" % (self._curve.canonical, x, y, extra)
+ return result
+
+ def has_private(self):
+ """``True`` if this key can be used for making signatures or decrypting data."""
+
+ return self._d is not None
+
+ # ECDSA
+ def _sign(self, z, k):
+ assert 0 < k < self._curve.order
+
+ order = self._curve.order
+ blind = Integer.random_range(min_inclusive=1,
+ max_exclusive=order)
+
+ blind_d = self._d * blind
+ inv_blind_k = (blind * k).inverse(order)
+
+ r = (self._curve.G * k).x % order
+ s = inv_blind_k * (blind * z + blind_d * r) % order
+ return (r, s)
+
+ # ECDSA
+ def _verify(self, z, rs):
+ order = self._curve.order
+ sinv = rs[1].inverse(order)
+ point1 = self._curve.G * ((sinv * z) % order)
+ point2 = self.pointQ * ((sinv * rs[0]) % order)
+ return (point1 + point2).x == rs[0]
+
+ @property
+ def d(self):
+ if not self.has_private():
+ raise ValueError("This is not a private ECC key")
+ return self._d
+
+ @property
+ def seed(self):
+ if not self.has_private():
+ raise ValueError("This is not a private ECC key")
+ return self._seed
+
+ @property
+ def pointQ(self):
+ if self._point is None:
+ self._point = self._curve.G * self._d
+ return self._point
+
+ def public_key(self):
+ """A matching ECC public key.
+
+ Returns:
+ a new :class:`EccKey` object
+ """
+
+ return EccKey(curve=self._curve.canonical, point=self.pointQ)
+
+ def _export_SEC1(self, compress):
+ if not self._curve.is_weierstrass:
+ raise ValueError("SEC1 format is only supported for NIST P curves")
+
+ # See 2.2 in RFC5480 and 2.3.3 in SEC1
+ #
+ # The first byte is:
+ # - 0x02: compressed, only X-coordinate, Y-coordinate is even
+ # - 0x03: compressed, only X-coordinate, Y-coordinate is odd
+ # - 0x04: uncompressed, X-coordinate is followed by Y-coordinate
+ #
+ # PAI is in theory encoded as 0x00.
+
+ modulus_bytes = self.pointQ.size_in_bytes()
+
+ if compress:
+ if self.pointQ.y.is_odd():
+ first_byte = b'\x03'
+ else:
+ first_byte = b'\x02'
+ public_key = (first_byte +
+ self.pointQ.x.to_bytes(modulus_bytes))
+ else:
+ public_key = (b'\x04' +
+ self.pointQ.x.to_bytes(modulus_bytes) +
+ self.pointQ.y.to_bytes(modulus_bytes))
+ return public_key
+
+ def _export_eddsa_public(self):
+ x, y = self.pointQ.xy
+ if self._curve.id == _CurveID.ED25519:
+ result = bytearray(y.to_bytes(32, byteorder='little'))
+ result[31] = ((x & 1) << 7) | result[31]
+ elif self._curve.id == _CurveID.ED448:
+ result = bytearray(y.to_bytes(57, byteorder='little'))
+ result[56] = (x & 1) << 7
+ else:
+ raise ValueError("Not an EdDSA key to export")
+ return bytes(result)
+
+ def _export_montgomery_public(self):
+ if not self._curve.is_montgomery:
+ raise ValueError("Not a Montgomery key to export")
+ x = self.pointQ.x
+ field_size = self.pointQ.size_in_bytes()
+ result = bytearray(x.to_bytes(field_size, byteorder='little'))
+ return bytes(result)
+
+ def _export_subjectPublicKeyInfo(self, compress):
+ if self._curve.is_edwards:
+ oid = self._curve.oid
+ public_key = self._export_eddsa_public()
+ params = None
+ elif self._curve.is_montgomery:
+ oid = self._curve.oid
+ public_key = self._export_montgomery_public()
+ params = None
+ else:
+ oid = "1.2.840.10045.2.1" # unrestricted
+ public_key = self._export_SEC1(compress)
+ params = DerObjectId(self._curve.oid)
+
+ return _create_subject_public_key_info(oid,
+ public_key,
+ params)
+
+ def _export_rfc5915_private_der(self, include_ec_params=True):
+
+ assert self.has_private()
+
+ # ECPrivateKey ::= SEQUENCE {
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
+ # privateKey OCTET STRING,
+ # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
+ # publicKey [1] BIT STRING OPTIONAL
+ # }
+
+ # Public key - uncompressed form
+ modulus_bytes = self.pointQ.size_in_bytes()
+ public_key = (b'\x04' +
+ self.pointQ.x.to_bytes(modulus_bytes) +
+ self.pointQ.y.to_bytes(modulus_bytes))
+
+ seq = [1,
+ DerOctetString(self.d.to_bytes(modulus_bytes)),
+ DerObjectId(self._curve.oid, explicit=0),
+ DerBitString(public_key, explicit=1)]
+
+ if not include_ec_params:
+ del seq[2]
+
+ return DerSequence(seq).encode()
+
+ def _export_pkcs8(self, **kwargs):
+ from Crypto.IO import PKCS8
+
+ if kwargs.get('passphrase', None) is not None and 'protection' not in kwargs:
+ raise ValueError("At least the 'protection' parameter must be present")
+
+ if self._seed is not None:
+ oid = self._curve.oid
+ private_key = DerOctetString(self._seed).encode()
+ params = None
+ else:
+ oid = "1.2.840.10045.2.1" # unrestricted
+ private_key = self._export_rfc5915_private_der(include_ec_params=False)
+ params = DerObjectId(self._curve.oid)
+
+ result = PKCS8.wrap(private_key,
+ oid,
+ key_params=params,
+ **kwargs)
+ return result
+
+ def _export_public_pem(self, compress):
+ from Crypto.IO import PEM
+
+ encoded_der = self._export_subjectPublicKeyInfo(compress)
+ return PEM.encode(encoded_der, "PUBLIC KEY")
+
+ def _export_private_pem(self, passphrase, **kwargs):
+ from Crypto.IO import PEM
+
+ encoded_der = self._export_rfc5915_private_der()
+ return PEM.encode(encoded_der, "EC PRIVATE KEY", passphrase, **kwargs)
+
+ def _export_private_clear_pkcs8_in_clear_pem(self):
+ from Crypto.IO import PEM
+
+ encoded_der = self._export_pkcs8()
+ return PEM.encode(encoded_der, "PRIVATE KEY")
+
+ def _export_private_encrypted_pkcs8_in_clear_pem(self, passphrase, **kwargs):
+ from Crypto.IO import PEM
+
+ assert passphrase
+ if 'protection' not in kwargs:
+ raise ValueError("At least the 'protection' parameter should be present")
+ encoded_der = self._export_pkcs8(passphrase=passphrase, **kwargs)
+ return PEM.encode(encoded_der, "ENCRYPTED PRIVATE KEY")
+
+ def _export_openssh(self, compress):
+ if self.has_private():
+ raise ValueError("Cannot export OpenSSH private keys")
+
+ desc = self._curve.openssh
+
+ if desc is None:
+ raise ValueError("Cannot export %s keys as OpenSSH" % self.curve)
+ elif desc == "ssh-ed25519":
+ public_key = self._export_eddsa_public()
+ comps = (tobytes(desc), tobytes(public_key))
+ else:
+ modulus_bytes = self.pointQ.size_in_bytes()
+
+ if compress:
+ first_byte = 2 + self.pointQ.y.is_odd()
+ public_key = (bchr(first_byte) +
+ self.pointQ.x.to_bytes(modulus_bytes))
+ else:
+ public_key = (b'\x04' +
+ self.pointQ.x.to_bytes(modulus_bytes) +
+ self.pointQ.y.to_bytes(modulus_bytes))
+
+ middle = desc.split("-")[2]
+ comps = (tobytes(desc), tobytes(middle), public_key)
+
+ blob = b"".join([struct.pack(">I", len(x)) + x for x in comps])
+ return desc + " " + tostr(binascii.b2a_base64(blob))
+
+ def export_key(self, **kwargs):
+ """Export this ECC key.
+
+ Args:
+ format (string):
+ The output format:
+
+ - ``'DER'``. The key will be encoded in ASN.1 DER format (binary).
+ For a public key, the ASN.1 ``subjectPublicKeyInfo`` structure
+ defined in `RFC5480`_ will be used.
+ For a private key, the ASN.1 ``ECPrivateKey`` structure defined
+ in `RFC5915`_ is used instead (possibly within a PKCS#8 envelope,
+ see the ``use_pkcs8`` flag below).
+ - ``'PEM'``. The key will be encoded in a PEM_ envelope (ASCII).
+ - ``'OpenSSH'``. The key will be encoded in the OpenSSH_ format
+ (ASCII, public keys only).
+ - ``'SEC1'``. The public key (i.e., the EC point) will be encoded
+ into ``bytes`` according to Section 2.3.3 of `SEC1`_
+ (which is a subset of the older X9.62 ITU standard).
+ Only for NIST P-curves.
+ - ``'raw'``. The public key will be encoded as ``bytes``,
+ without any metadata.
+
+ * For NIST P-curves: equivalent to ``'SEC1'``.
+ * For Ed25519 and Ed448: ``bytes`` in the format
+ defined in `RFC8032`_.
+ * For Curve25519 and Curve448: ``bytes`` in the format
+ defined in `RFC7748`_.
+
+ passphrase (bytes or string):
+ (*Private keys only*) The passphrase to protect the
+ private key.
+
+ use_pkcs8 (boolean):
+ (*Private keys only*)
+ If ``True`` (default and recommended), the `PKCS#8`_ representation
+ will be used.
+ It must be ``True`` for Ed25519, Ed448, Curve25519, and Curve448.
+
+ If ``False`` and a passphrase is present, the obsolete PEM
+ encryption will be used.
+
+ protection (string):
+ When a private key is exported with password-protection
+ and PKCS#8 (both ``DER`` and ``PEM`` formats), this parameter MUST be
+ present,
+ For all possible protection schemes,
+ refer to :ref:`the encryption parameters of PKCS#8`.
+ It is recommended to use ``'PBKDF2WithHMAC-SHA512AndAES128-CBC'``.
+
+ compress (boolean):
+ If ``True``, the method returns a more compact representation
+ of the public key, with the X-coordinate only.
+
+ If ``False`` (default), the method returns the full public key.
+
+ This parameter is ignored for Ed25519/Ed448/Curve25519/Curve448,
+ as compression is mandatory.
+
+ prot_params (dict):
+ When a private key is exported with password-protection
+ and PKCS#8 (both ``DER`` and ``PEM`` formats), this dictionary
+ contains the parameters to use to derive the encryption key
+ from the passphrase.
+ For all possible values,
+ refer to :ref:`the encryption parameters of PKCS#8`.
+ The recommendation is to use ``{'iteration_count':21000}`` for PBKDF2,
+ and ``{'iteration_count':131072}`` for scrypt.
+
+ .. warning::
+ If you don't provide a passphrase, the private key will be
+ exported in the clear!
+
+ .. note::
+ When exporting a private key with password-protection and `PKCS#8`_
+ (both ``DER`` and ``PEM`` formats), any extra parameters
+ to ``export_key()`` will be passed to :mod:`Crypto.IO.PKCS8`.
+
+ .. _PEM: http://www.ietf.org/rfc/rfc1421.txt
+ .. _`PEM encryption`: http://www.ietf.org/rfc/rfc1423.txt
+ .. _OpenSSH: http://www.openssh.com/txt/rfc5656.txt
+ .. _RFC5480: https://tools.ietf.org/html/rfc5480
+ .. _SEC1: https://www.secg.org/sec1-v2.pdf
+ .. _RFC7748: https://tools.ietf.org/html/rfc7748
+
+ Returns:
+ A multi-line string (for ``'PEM'`` and ``'OpenSSH'``) or
+ ``bytes`` (for ``'DER'``, ``'SEC1'``, and ``'raw'``) with the encoded key.
+ """
+
+ args = kwargs.copy()
+ ext_format = args.pop("format")
+ if ext_format not in ("PEM", "DER", "OpenSSH", "SEC1", "raw"):
+ raise ValueError("Unknown format '%s'" % ext_format)
+
+ compress = args.pop("compress", False)
+
+ if self.has_private():
+ passphrase = args.pop("passphrase", None)
+ if is_string(passphrase):
+ passphrase = tobytes(passphrase)
+ if not passphrase:
+ raise ValueError("Empty passphrase")
+
+ use_pkcs8 = args.pop("use_pkcs8", True)
+ if use_pkcs8 is False:
+ if self._curve.is_edwards:
+ raise ValueError("'pkcs8' must be True for EdDSA curves")
+ if self._curve.is_montgomery:
+ raise ValueError("'pkcs8' must be True for Curve25519")
+ if 'protection' in args:
+ raise ValueError("'protection' is only supported for PKCS#8")
+
+ if ext_format == "PEM":
+ if use_pkcs8:
+ if passphrase:
+ return self._export_private_encrypted_pkcs8_in_clear_pem(passphrase, **args)
+ else:
+ return self._export_private_clear_pkcs8_in_clear_pem()
+ else:
+ return self._export_private_pem(passphrase, **args)
+ elif ext_format == "DER":
+ # DER
+ if passphrase and not use_pkcs8:
+ raise ValueError("Private keys can only be encrpyted with DER using PKCS#8")
+ if use_pkcs8:
+ return self._export_pkcs8(passphrase=passphrase, **args)
+ else:
+ return self._export_rfc5915_private_der()
+ else:
+ raise ValueError("Private keys cannot be exported "
+ "in the '%s' format" % ext_format)
+ else: # Public key
+ if args:
+ raise ValueError("Unexpected parameters: '%s'" % args)
+ if ext_format == "PEM":
+ return self._export_public_pem(compress)
+ elif ext_format == "DER":
+ return self._export_subjectPublicKeyInfo(compress)
+ elif ext_format == "SEC1":
+ return self._export_SEC1(compress)
+ elif ext_format == "raw":
+ if self._curve.is_edwards:
+ return self._export_eddsa_public()
+ elif self._curve.is_montgomery:
+ return self._export_montgomery_public()
+ else:
+ return self._export_SEC1(compress)
+ else:
+ return self._export_openssh(compress)
+
+
+def generate(**kwargs):
+ """Generate a new private key on the given curve.
+
+ Args:
+
+ curve (string):
+ Mandatory. It must be a curve name defined in the `ECC table`_.
+
+ randfunc (callable):
+ Optional. The RNG to read randomness from.
+ If ``None``, :func:`Crypto.Random.get_random_bytes` is used.
+ """
+
+ curve_name = kwargs.pop("curve")
+ curve = _curves[curve_name]
+ randfunc = kwargs.pop("randfunc", get_random_bytes)
+ if kwargs:
+ raise TypeError("Unknown parameters: " + str(kwargs))
+
+ if _curves[curve_name].id == _CurveID.ED25519:
+ seed = randfunc(32)
+ new_key = EccKey(curve=curve_name, seed=seed)
+ elif _curves[curve_name].id == _CurveID.ED448:
+ seed = randfunc(57)
+ new_key = EccKey(curve=curve_name, seed=seed)
+ elif _curves[curve_name].id == _CurveID.CURVE25519:
+ seed = randfunc(32)
+ new_key = EccKey(curve=curve_name, seed=seed)
+ _curves[curve_name].validate(new_key.pointQ)
+ elif _curves[curve_name].id == _CurveID.CURVE448:
+ seed = randfunc(56)
+ new_key = EccKey(curve=curve_name, seed=seed)
+ _curves[curve_name].validate(new_key.pointQ)
+ else:
+ d = Integer.random_range(min_inclusive=1,
+ max_exclusive=curve.order,
+ randfunc=randfunc)
+ new_key = EccKey(curve=curve_name, d=d)
+
+ return new_key
+
+
+def construct(**kwargs):
+ """Build a new ECC key (private or public) starting
+ from some base components.
+
+ In most cases, you will already have an existing key
+ which you can read in with :func:`import_key` instead
+ of this function.
+
+ Args:
+ curve (string):
+ Mandatory. The name of the elliptic curve, as defined in the `ECC table`_.
+
+ d (integer):
+ Mandatory for a private key and a NIST P-curve (e.g., P-256).
+ It must be an integer in the range ``[1..order-1]``.
+
+ seed (bytes):
+ Mandatory for a private key and curves Ed25519 (32 bytes),
+ Curve25519 (32 bytes), Curve448 (56 bytes) and Ed448 (57 bytes).
+
+ point_x (integer):
+ The X coordinate (affine) of the ECC point.
+ Mandatory for a public key.
+
+ point_y (integer):
+ The Y coordinate (affine) of the ECC point.
+ Mandatory for a public key,
+ except for Curve25519 and Curve448.
+
+ Returns:
+ :class:`EccKey` : a new ECC key object
+ """
+
+ curve_name = kwargs["curve"]
+ curve = _curves[curve_name]
+ point_x = kwargs.pop("point_x", None)
+ point_y = kwargs.pop("point_y", None)
+
+ if "point" in kwargs:
+ raise TypeError("Unknown keyword: point")
+
+ if curve.id == _CurveID.CURVE25519:
+
+ if point_x is not None:
+ kwargs["point"] = EccXPoint(point_x, curve_name)
+ new_key = EccKey(**kwargs)
+ curve.validate(new_key.pointQ)
+
+ elif curve.id == _CurveID.CURVE448:
+
+ if point_x is not None:
+ kwargs["point"] = EccXPoint(point_x, curve_name)
+ new_key = EccKey(**kwargs)
+ curve.validate(new_key.pointQ)
+
+ else:
+
+ if None not in (point_x, point_y):
+ kwargs["point"] = EccPoint(point_x, point_y, curve_name)
+ new_key = EccKey(**kwargs)
+
+ # Validate that the private key matches the public one
+ # because EccKey will not do that automatically
+ if new_key.has_private() and 'point' in kwargs:
+ pub_key = curve.G * new_key.d
+ if pub_key.xy != (point_x, point_y):
+ raise ValueError("Private and public ECC keys do not match")
+
+ return new_key
+
+
+def _import_public_der(ec_point, curve_oid=None, curve_name=None):
+ """Convert an encoded EC point into an EccKey object
+
+ ec_point: byte string with the EC point (SEC1-encoded)
+ curve_oid: string with the name the curve
+ curve_name: string with the OID of the curve
+
+ Either curve_id or curve_name must be specified
+
+ """
+
+ for _curve_name, curve in _curves.items():
+ if curve_oid and curve.oid == curve_oid:
+ break
+ if curve_name == _curve_name:
+ break
+ else:
+ if curve_oid:
+ raise UnsupportedEccFeature("Unsupported ECC curve (OID: %s)" % curve_oid)
+ else:
+ raise UnsupportedEccFeature("Unsupported ECC curve (%s)" % curve_name)
+
+ # See 2.2 in RFC5480 and 2.3.3 in SEC1
+ # The first byte is:
+ # - 0x02: compressed, only X-coordinate, Y-coordinate is even
+ # - 0x03: compressed, only X-coordinate, Y-coordinate is odd
+ # - 0x04: uncompressed, X-coordinate is followed by Y-coordinate
+ #
+ # PAI is in theory encoded as 0x00.
+
+ modulus_bytes = curve.p.size_in_bytes()
+ point_type = bord(ec_point[0])
+
+ # Uncompressed point
+ if point_type == 0x04:
+ if len(ec_point) != (1 + 2 * modulus_bytes):
+ raise ValueError("Incorrect EC point length")
+ x = Integer.from_bytes(ec_point[1:modulus_bytes+1])
+ y = Integer.from_bytes(ec_point[modulus_bytes+1:])
+ # Compressed point
+ elif point_type in (0x02, 0x03):
+ if len(ec_point) != (1 + modulus_bytes):
+ raise ValueError("Incorrect EC point length")
+ x = Integer.from_bytes(ec_point[1:])
+ # Right now, we only support Short Weierstrass curves
+ y = (x**3 - x*3 + curve.b).sqrt(curve.p)
+ if point_type == 0x02 and y.is_odd():
+ y = curve.p - y
+ if point_type == 0x03 and y.is_even():
+ y = curve.p - y
+ else:
+ raise ValueError("Incorrect EC point encoding")
+
+ return construct(curve=_curve_name, point_x=x, point_y=y)
+
+
+def _import_subjectPublicKeyInfo(encoded, *kwargs):
+ """Convert a subjectPublicKeyInfo into an EccKey object"""
+
+ # See RFC5480
+
+ # Parse the generic subjectPublicKeyInfo structure
+ oid, ec_point, params = _expand_subject_public_key_info(encoded)
+
+ nist_p_oids = (
+ "1.2.840.10045.2.1", # id-ecPublicKey (unrestricted)
+ "1.3.132.1.12", # id-ecDH
+ "1.3.132.1.13" # id-ecMQV
+ )
+ eddsa_oids = {
+ "1.3.101.112": ("Ed25519", _import_ed25519_public_key), # id-Ed25519
+ "1.3.101.113": ("Ed448", _import_ed448_public_key) # id-Ed448
+ }
+ xdh_oids = {
+ "1.3.101.110": ("Curve25519", _import_curve25519_public_key), # id-X25519
+ "1.3.101.111": ("Curve448", _import_curve448_public_key), # id-X448
+ }
+
+ if oid in nist_p_oids:
+ # See RFC5480
+
+ # Parameters are mandatory and encoded as ECParameters
+ # ECParameters ::= CHOICE {
+ # namedCurve OBJECT IDENTIFIER
+ # -- implicitCurve NULL
+ # -- specifiedCurve SpecifiedECDomain
+ # }
+ # implicitCurve and specifiedCurve are not supported (as per RFC)
+ if not params:
+ raise ValueError("Missing ECC parameters for ECC OID %s" % oid)
+ try:
+ curve_oid = DerObjectId().decode(params).value
+ except ValueError:
+ raise ValueError("Error decoding namedCurve")
+
+ # ECPoint ::= OCTET STRING
+ return _import_public_der(ec_point, curve_oid=curve_oid)
+
+ elif oid in eddsa_oids:
+ # See RFC8410
+ curve_name, import_eddsa_public_key = eddsa_oids[oid]
+
+ # Parameters must be absent
+ if params:
+ raise ValueError("Unexpected ECC parameters for ECC OID %s" % oid)
+
+ x, y = import_eddsa_public_key(ec_point)
+ return construct(point_x=x, point_y=y, curve=curve_name)
+
+ elif oid in xdh_oids:
+ curve_name, import_xdh_public_key = xdh_oids[oid]
+
+ # Parameters must be absent
+ if params:
+ raise ValueError("Unexpected ECC parameters for ECC OID %s" % oid)
+
+ x = import_xdh_public_key(ec_point)
+ return construct(point_x=x, curve=curve_name)
+
+ else:
+ raise UnsupportedEccFeature("Unsupported ECC OID: %s" % oid)
+
+
+def _import_rfc5915_der(encoded, passphrase, curve_oid=None):
+
+ # See RFC5915 https://tools.ietf.org/html/rfc5915
+ #
+ # ECPrivateKey ::= SEQUENCE {
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
+ # privateKey OCTET STRING,
+ # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
+ # publicKey [1] BIT STRING OPTIONAL
+ # }
+
+ ec_private_key = DerSequence().decode(encoded, nr_elements=(2, 3, 4))
+ if ec_private_key[0] != 1:
+ raise ValueError("Incorrect ECC private key version")
+
+ scalar_bytes = DerOctetString().decode(ec_private_key[1]).payload
+
+ next_element = 2
+
+ # Try to decode 'parameters'
+ if next_element < len(ec_private_key):
+ try:
+ parameters = DerObjectId(explicit=0).decode(ec_private_key[next_element]).value
+ if curve_oid is not None and parameters != curve_oid:
+ raise ValueError("Curve mismatch")
+ curve_oid = parameters
+ next_element += 1
+ except ValueError:
+ pass
+
+ if curve_oid is None:
+ raise ValueError("No curve found")
+
+ for curve_name, curve in _curves.items():
+ if curve.oid == curve_oid:
+ break
+ else:
+ raise UnsupportedEccFeature("Unsupported ECC curve (OID: %s)" % curve_oid)
+
+ modulus_bytes = curve.p.size_in_bytes()
+ if len(scalar_bytes) != modulus_bytes:
+ raise ValueError("Private key is too small")
+
+ # Try to decode 'publicKey'
+ point_x = point_y = None
+ if next_element < len(ec_private_key):
+ try:
+ public_key_enc = DerBitString(explicit=1).decode(ec_private_key[next_element]).value
+ public_key = _import_public_der(public_key_enc, curve_oid=curve_oid)
+ point_x = public_key.pointQ.x
+ point_y = public_key.pointQ.y
+ next_element += 1
+ except ValueError:
+ pass
+
+ d = Integer.from_bytes(scalar_bytes)
+ return construct(curve=curve_name, d=d, point_x=point_x, point_y=point_y)
+
+
+def _import_pkcs8(encoded, passphrase):
+ from Crypto.IO import PKCS8
+
+ algo_oid, private_key, params = PKCS8.unwrap(encoded, passphrase)
+
+ nist_p_oids = (
+ "1.2.840.10045.2.1", # id-ecPublicKey (unrestricted)
+ "1.3.132.1.12", # id-ecDH
+ "1.3.132.1.13" # id-ecMQV
+ )
+ eddsa_oids = {
+ "1.3.101.112": "Ed25519", # id-Ed25519
+ "1.3.101.113": "Ed448", # id-Ed448
+ }
+ xdh_oids = {
+ "1.3.101.110": "Curve25519", # id-X25519
+ "1.3.101.111": "Curve448", # id-X448
+ }
+
+ if algo_oid in nist_p_oids:
+ curve_oid = DerObjectId().decode(params).value
+ return _import_rfc5915_der(private_key, passphrase, curve_oid)
+ elif algo_oid in eddsa_oids:
+ if params is not None:
+ raise ValueError("EdDSA ECC private key must not have parameters")
+ curve_oid = None
+ seed = DerOctetString().decode(private_key).payload
+ return construct(curve=eddsa_oids[algo_oid], seed=seed)
+ elif algo_oid in xdh_oids:
+ curve_name = xdh_oids[algo_oid]
+ if params is not None:
+ raise ValueError("%s ECC private key must not have parameters" %
+ curve_name)
+ curve_oid = None
+ seed = DerOctetString().decode(private_key).payload
+ return construct(curve=xdh_oids[algo_oid], seed=seed)
+ else:
+ raise UnsupportedEccFeature("Unsupported ECC purpose (OID: %s)" % algo_oid)
+
+
+def _import_x509_cert(encoded, *kwargs):
+
+ sp_info = _extract_subject_public_key_info(encoded)
+ return _import_subjectPublicKeyInfo(sp_info)
+
+
+def _import_der(encoded, passphrase):
+
+ try:
+ return _import_subjectPublicKeyInfo(encoded, passphrase)
+ except UnsupportedEccFeature as err:
+ raise err
+ except (ValueError, TypeError, IndexError):
+ pass
+
+ try:
+ return _import_x509_cert(encoded, passphrase)
+ except UnsupportedEccFeature as err:
+ raise err
+ except (ValueError, TypeError, IndexError):
+ pass
+
+ try:
+ return _import_rfc5915_der(encoded, passphrase)
+ except UnsupportedEccFeature as err:
+ raise err
+ except (ValueError, TypeError, IndexError):
+ pass
+
+ try:
+ return _import_pkcs8(encoded, passphrase)
+ except UnsupportedEccFeature as err:
+ raise err
+ except (ValueError, TypeError, IndexError):
+ pass
+
+ raise ValueError("Not an ECC DER key")
+
+
+def _import_openssh_public(encoded):
+ parts = encoded.split(b' ')
+ if len(parts) not in (2, 3):
+ raise ValueError("Not an openssh public key")
+
+ try:
+ keystring = binascii.a2b_base64(parts[1])
+
+ keyparts = []
+ while len(keystring) > 4:
+ lk = struct.unpack(">I", keystring[:4])[0]
+ keyparts.append(keystring[4:4 + lk])
+ keystring = keystring[4 + lk:]
+
+ if parts[0] != keyparts[0]:
+ raise ValueError("Mismatch in openssh public key")
+
+ # NIST P curves
+ if parts[0].startswith(b"ecdsa-sha2-"):
+
+ for curve_name, curve in _curves.items():
+ if curve.openssh is None:
+ continue
+ if not curve.openssh.startswith("ecdsa-sha2"):
+ continue
+ middle = tobytes(curve.openssh.split("-")[2])
+ if keyparts[1] == middle:
+ break
+ else:
+ raise ValueError("Unsupported ECC curve: " + middle)
+
+ ecc_key = _import_public_der(keyparts[2], curve_oid=curve.oid)
+
+ # EdDSA
+ elif parts[0] == b"ssh-ed25519":
+ x, y = _import_ed25519_public_key(keyparts[1])
+ ecc_key = construct(curve="Ed25519", point_x=x, point_y=y)
+ else:
+ raise ValueError("Unsupported SSH key type: " + parts[0])
+
+ except (IndexError, TypeError, binascii.Error):
+ raise ValueError("Error parsing SSH key type: " + parts[0])
+
+ return ecc_key
+
+
+def _import_openssh_private_ecc(data, password):
+
+ from ._openssh import (import_openssh_private_generic,
+ read_bytes, read_string, check_padding)
+
+ key_type, decrypted = import_openssh_private_generic(data, password)
+
+ eddsa_keys = {
+ "ssh-ed25519": ("Ed25519", _import_ed25519_public_key, 32),
+ }
+
+ # https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-04
+ if key_type.startswith("ecdsa-sha2"):
+
+ ecdsa_curve_name, decrypted = read_string(decrypted)
+ if ecdsa_curve_name not in _curves:
+ raise UnsupportedEccFeature("Unsupported ECC curve %s" % ecdsa_curve_name)
+ curve = _curves[ecdsa_curve_name]
+ modulus_bytes = (curve.modulus_bits + 7) // 8
+
+ public_key, decrypted = read_bytes(decrypted)
+
+ if bord(public_key[0]) != 4:
+ raise ValueError("Only uncompressed OpenSSH EC keys are supported")
+ if len(public_key) != 2 * modulus_bytes + 1:
+ raise ValueError("Incorrect public key length")
+
+ point_x = Integer.from_bytes(public_key[1:1+modulus_bytes])
+ point_y = Integer.from_bytes(public_key[1+modulus_bytes:])
+
+ private_key, decrypted = read_bytes(decrypted)
+ d = Integer.from_bytes(private_key)
+
+ params = {'d': d, 'curve': ecdsa_curve_name}
+
+ elif key_type in eddsa_keys:
+
+ curve_name, import_eddsa_public_key, seed_len = eddsa_keys[key_type]
+
+ public_key, decrypted = read_bytes(decrypted)
+ point_x, point_y = import_eddsa_public_key(public_key)
+
+ private_public_key, decrypted = read_bytes(decrypted)
+ seed = private_public_key[:seed_len]
+
+ params = {'seed': seed, 'curve': curve_name}
+ else:
+ raise ValueError("Unsupport SSH agent key type:" + key_type)
+
+ _, padded = read_string(decrypted) # Comment
+ check_padding(padded)
+
+ return construct(point_x=point_x, point_y=point_y, **params)
+
+
+def _import_ed25519_public_key(encoded):
+ """Import an Ed25519 ECC public key, encoded as raw bytes as described
+ in RFC8032_.
+
+ Args:
+ encoded (bytes):
+ The Ed25519 public key to import. It must be 32 bytes long.
+
+ Returns:
+ x and y (integer)
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+
+ .. _RFC8032: https://datatracker.ietf.org/doc/html/rfc8032
+ """
+
+ if len(encoded) != 32:
+ raise ValueError("Incorrect length. Only Ed25519 public keys are supported.")
+
+ p = Integer(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed) # 2**255 - 19
+ d = 37095705934669439343138083508754565189542113879843219016388785533085940283555
+
+ y = bytearray(encoded)
+ x_lsb = y[31] >> 7
+ y[31] &= 0x7F
+ point_y = Integer.from_bytes(y, byteorder='little')
+ if point_y >= p:
+ raise ValueError("Invalid Ed25519 key (y)")
+ if point_y == 1:
+ return 0, 1
+
+ u = (point_y**2 - 1) % p
+ v = ((point_y**2 % p) * d + 1) % p
+ try:
+ v_inv = v.inverse(p)
+ x2 = (u * v_inv) % p
+ point_x = Integer._tonelli_shanks(x2, p)
+ if (point_x & 1) != x_lsb:
+ point_x = p - point_x
+ except ValueError:
+ raise ValueError("Invalid Ed25519 public key")
+ return point_x, point_y
+
+
+def _import_curve25519_public_key(encoded):
+ """Import a Curve25519 ECC public key,
+ encoded as raw bytes as described in RFC7748_.
+
+ Args:
+ encoded (bytes):
+ The Curve25519 public key to import. It must be 32 bytes long.
+
+ Returns:
+ x (integer)
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+
+ .. _RFC7748: https://datatracker.ietf.org/doc/html/rfc7748
+ """
+
+ if len(encoded) != 32:
+ raise ValueError("Incorrect Curve25519 key length")
+
+ x = bytearray(encoded)
+ # RFC 7741, Section 5
+ x[31] &= 0x7F
+ point_x = Integer.from_bytes(x, byteorder='little')
+
+ return point_x
+
+
+def _import_curve448_public_key(encoded):
+ """Import a Curve448 ECC public key,
+ encoded as raw bytes as described in RFC7748_.
+
+ Args:
+ encoded (bytes):
+ The Curve448 public key to import. It must be 56 bytes long.
+
+ Returns:
+ x (integer)
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+
+ .. _RFC7748: https://datatracker.ietf.org/doc/html/rfc7748
+ """
+
+ if len(encoded) != 56:
+ raise ValueError("Incorrect Curve448 key length")
+
+ point_x = Integer.from_bytes(encoded, byteorder='little')
+
+ return point_x
+
+
+def _import_ed448_public_key(encoded):
+ """Import an Ed448 ECC public key, encoded as raw bytes as described
+ in RFC8032_.
+
+ Args:
+ encoded (bytes):
+ The Ed448 public key to import. It must be 57 bytes long.
+
+ Returns:
+ x and y (integer)
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+
+ .. _RFC8032: https://datatracker.ietf.org/doc/html/rfc8032
+ """
+
+ if len(encoded) != 57:
+ raise ValueError("Incorrect length. Only Ed448 public keys are supported.")
+
+ p = _curves['curve448'].p
+ d = p - 39081
+
+ y = encoded[:56]
+ x_lsb = bord(encoded[56]) >> 7
+ point_y = Integer.from_bytes(y, byteorder='little')
+ if point_y >= p:
+ raise ValueError("Invalid Ed448 key (y)")
+ if point_y == 1:
+ return 0, 1
+
+ u = (point_y**2 - 1) % p
+ v = ((point_y**2 % p) * d - 1) % p
+ try:
+ v_inv = v.inverse(p)
+ x2 = (u * v_inv) % p
+ point_x = Integer._tonelli_shanks(x2, p)
+ if (point_x & 1) != x_lsb:
+ point_x = p - point_x
+ except ValueError:
+ raise ValueError("Invalid Ed448 public key")
+ return point_x, point_y
+
+
+def import_key(encoded, passphrase=None, curve_name=None):
+ """Import an ECC key (public or private).
+
+ Args:
+ encoded (bytes or multi-line string):
+ The ECC key to import.
+ The function will try to automatically detect the right format.
+
+ Supported formats for an ECC **public** key:
+
+ * X.509 certificate: binary (DER) or ASCII (PEM).
+ * X.509 ``subjectPublicKeyInfo``: binary (DER) or ASCII (PEM).
+ * SEC1_ (or X9.62), as ``bytes``. NIST P curves only.
+ You must also provide the ``curve_name`` (with a value from the `ECC table`_)
+ * OpenSSH line, defined in RFC5656_ and RFC8709_ (ASCII).
+ This is normally the content of files like ``~/.ssh/id_ecdsa.pub``.
+
+ Supported formats for an ECC **private** key:
+
+ * A binary ``ECPrivateKey`` structure, as defined in `RFC5915`_ (DER).
+ NIST P curves only.
+ * A `PKCS#8`_ structure (or the more recent Asymmetric Key
+ Package, RFC5958_): binary (DER) or ASCII (PEM).
+ * `OpenSSH 6.5`_ and newer versions (ASCII).
+
+ Private keys can be in the clear or password-protected.
+
+ For details about the PEM encoding, see `RFC1421`_/`RFC1423`_.
+
+ passphrase (byte string):
+ The passphrase to use for decrypting a private key.
+ Encryption may be applied protected at the PEM level (not recommended)
+ or at the PKCS#8 level (recommended).
+ This parameter is ignored if the key in input is not encrypted.
+
+ curve_name (string):
+ For a SEC1 encoding only. This is the name of the curve,
+ as defined in the `ECC table`_.
+
+ .. note::
+
+ To import EdDSA private and public keys, when encoded as raw ``bytes``, use:
+
+ * :func:`Crypto.Signature.eddsa.import_public_key`, or
+ * :func:`Crypto.Signature.eddsa.import_private_key`.
+
+ .. note::
+
+ To import X25519/X448 private and public keys, when encoded as raw ``bytes``, use:
+
+ * :func:`Crypto.Protocol.DH.import_x25519_public_key`
+ * :func:`Crypto.Protocol.DH.import_x25519_private_key`
+ * :func:`Crypto.Protocol.DH.import_x448_public_key`
+ * :func:`Crypto.Protocol.DH.import_x448_private_key`
+
+ Returns:
+ :class:`EccKey` : a new ECC key object
+
+ Raises:
+ ValueError: when the given key cannot be parsed (possibly because
+ the pass phrase is wrong).
+
+ .. _RFC1421: https://datatracker.ietf.org/doc/html/rfc1421
+ .. _RFC1423: https://datatracker.ietf.org/doc/html/rfc1423
+ .. _RFC5915: https://datatracker.ietf.org/doc/html/rfc5915
+ .. _RFC5656: https://datatracker.ietf.org/doc/html/rfc5656
+ .. _RFC8709: https://datatracker.ietf.org/doc/html/rfc8709
+ .. _RFC5958: https://datatracker.ietf.org/doc/html/rfc5958
+ .. _`PKCS#8`: https://datatracker.ietf.org/doc/html/rfc5208
+ .. _`OpenSSH 6.5`: https://flak.tedunangst.com/post/new-openssh-key-format-and-bcrypt-pbkdf
+ .. _SEC1: https://www.secg.org/sec1-v2.pdf
+ """
+
+ from Crypto.IO import PEM
+
+ encoded = tobytes(encoded)
+ if passphrase is not None:
+ passphrase = tobytes(passphrase)
+
+ # PEM
+ if encoded.startswith(b'-----BEGIN OPENSSH PRIVATE KEY'):
+ text_encoded = tostr(encoded)
+ openssh_encoded, marker, enc_flag = PEM.decode(text_encoded, passphrase)
+ result = _import_openssh_private_ecc(openssh_encoded, passphrase)
+ return result
+
+ elif encoded.startswith(b'-----'):
+
+ text_encoded = tostr(encoded)
+
+ # Remove any EC PARAMETERS section
+ # Ignore its content because the curve type must be already given in the key
+ ecparams_start = "-----BEGIN EC PARAMETERS-----"
+ ecparams_end = "-----END EC PARAMETERS-----"
+ text_encoded = re.sub(ecparams_start + ".*?" + ecparams_end, "",
+ text_encoded,
+ flags=re.DOTALL)
+
+ der_encoded, marker, enc_flag = PEM.decode(text_encoded, passphrase)
+ if enc_flag:
+ passphrase = None
+ try:
+ result = _import_der(der_encoded, passphrase)
+ except UnsupportedEccFeature as uef:
+ raise uef
+ except ValueError:
+ raise ValueError("Invalid DER encoding inside the PEM file")
+ return result
+
+ # OpenSSH
+ if encoded.startswith((b'ecdsa-sha2-', b'ssh-ed25519')):
+ return _import_openssh_public(encoded)
+
+ # DER
+ if len(encoded) > 0 and bord(encoded[0]) == 0x30:
+ return _import_der(encoded, passphrase)
+
+ # SEC1
+ if len(encoded) > 0 and bord(encoded[0]) in (0x02, 0x03, 0x04):
+ if curve_name is None:
+ raise ValueError("No curve name was provided")
+ return _import_public_der(encoded, curve_name=curve_name)
+
+ raise ValueError("ECC key format is not supported")
+
+
+if __name__ == "__main__":
+
+ import time
+
+ d = 0xc51e4753afdec1e6b6c6a5b992f43f8dd0c7a8933072708b6522468b2ffb06fd
+
+ point = _curves['p256'].G.copy()
+ count = 3000
+
+ start = time.time()
+ for x in range(count):
+ pointX = point * d
+ print("(P-256 G)", (time.time() - start) / count * 1000, "ms")
+
+ start = time.time()
+ for x in range(count):
+ pointX = pointX * d
+ print("(P-256 arbitrary point)", (time.time() - start) / count * 1000, "ms")
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ECC.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ECC.pyi
new file mode 100644
index 00000000..e0e65172
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ECC.pyi
@@ -0,0 +1,80 @@
+from __future__ import annotations
+
+from typing import Union, Callable, Optional, Tuple, Dict, NamedTuple, Any, overload, Literal
+from typing_extensions import TypedDict, Unpack, NotRequired
+
+from Crypto.Math.Numbers import Integer
+from Crypto.IO._PBES import ProtParams
+
+from ._point import EccPoint as EccPoint
+from ._point import EccXPoint as EccXPoint
+
+RNG = Callable[[int], bytes]
+
+
+class UnsupportedEccFeature(ValueError):
+ ...
+
+class ExportParams(TypedDict):
+ passphrase: NotRequired[Union[bytes, str]]
+ use_pkcs8: NotRequired[bool]
+ protection: NotRequired[str]
+ compress: NotRequired[bool]
+ prot_params: NotRequired[ProtParams]
+
+
+class EccKey(object):
+ curve: str
+ def __init__(self, *, curve: str = ..., d: int = ..., point: EccPoint = ...) -> None: ...
+ def __eq__(self, other: object) -> bool: ...
+ def __repr__(self) -> str: ...
+ def has_private(self) -> bool: ...
+ @property
+ def d(self) -> int: ...
+ @property
+ def pointQ(self) -> EccPoint: ...
+ def public_key(self) -> EccKey: ...
+
+ @overload
+ def export_key(self,
+ *,
+ format: Literal['PEM', 'OpenSSH'],
+ **kwargs: Unpack[ExportParams]) -> str: ...
+
+ @overload
+ def export_key(self,
+ *,
+ format: Literal['DER', 'SEC1', 'raw'],
+ **kwargs: Unpack[ExportParams]) -> bytes: ...
+
+
+_Curve = NamedTuple("_Curve", [('p', Integer),
+ ('order', Integer),
+ ('b', Integer),
+ ('Gx', Integer),
+ ('Gy', Integer),
+ ('G', EccPoint),
+ ('modulus_bits', int),
+ ('oid', str),
+ ('context', Any),
+ ('desc', str),
+ ('openssh', Union[str, None]),
+ ])
+
+_curves: Dict[str, _Curve]
+
+def _import_rfc5915_der(encoded: bytes,
+ passphrase: Optional[str] = None,
+ curve_oid: Optional[str] = None) -> EccKey: ...
+
+def generate(**kwargs: Union[str, RNG]) -> EccKey: ...
+def construct(**kwargs: Union[str, int]) -> EccKey: ...
+
+
+def import_key(encoded: Union[bytes, str],
+ passphrase: Optional[str] = None,
+ curve_name: Optional[str] = None) -> EccKey: ...
+
+
+def _import_ed25519_public_key(encoded: bytes) -> EccKey: ...
+def _import_ed448_public_key(encoded: bytes) -> EccKey: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ElGamal.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ElGamal.py
new file mode 100644
index 00000000..3b108405
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ElGamal.py
@@ -0,0 +1,286 @@
+#
+# ElGamal.py : ElGamal encryption/decryption and signatures
+#
+# Part of the Python Cryptography Toolkit
+#
+# Originally written by: A.M. Kuchling
+#
+# ===================================================================
+# 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__ = ['generate', 'construct', 'ElGamalKey']
+
+from Crypto import Random
+from Crypto.Math.Primality import ( generate_probable_safe_prime,
+ test_probable_prime, COMPOSITE )
+from Crypto.Math.Numbers import Integer
+
+# Generate an ElGamal key with N bits
+def generate(bits, randfunc):
+ """Randomly generate a fresh, new ElGamal key.
+
+ The key will be safe for use for both encryption and signature
+ (although it should be used for **only one** purpose).
+
+ Args:
+ bits (int):
+ Key length, or size (in bits) of the modulus *p*.
+ The recommended value is 2048.
+ randfunc (callable):
+ Random number generation function; it should accept
+ a single integer *N* and return a string of random
+ *N* random bytes.
+
+ Return:
+ an :class:`ElGamalKey` object
+ """
+
+ obj=ElGamalKey()
+
+ # Generate a safe prime p
+ # See Algorithm 4.86 in Handbook of Applied Cryptography
+ obj.p = generate_probable_safe_prime(exact_bits=bits, randfunc=randfunc)
+ q = (obj.p - 1) >> 1
+
+ # Generate generator g
+ while 1:
+ # Choose a square residue; it will generate a cyclic group of order q.
+ obj.g = pow(Integer.random_range(min_inclusive=2,
+ max_exclusive=obj.p,
+ randfunc=randfunc), 2, obj.p)
+
+ # We must avoid g=2 because of Bleichenbacher's attack described
+ # in "Generating ElGamal signatures without knowning the secret key",
+ # 1996
+ if obj.g in (1, 2):
+ continue
+
+ # Discard g if it divides p-1 because of the attack described
+ # in Note 11.67 (iii) in HAC
+ if (obj.p - 1) % obj.g == 0:
+ continue
+
+ # g^{-1} must not divide p-1 because of Khadir's attack
+ # described in "Conditions of the generator for forging ElGamal
+ # signature", 2011
+ ginv = obj.g.inverse(obj.p)
+ if (obj.p - 1) % ginv == 0:
+ continue
+
+ # Found
+ break
+
+ # Generate private key x
+ obj.x = Integer.random_range(min_inclusive=2,
+ max_exclusive=obj.p-1,
+ randfunc=randfunc)
+ # Generate public key y
+ obj.y = pow(obj.g, obj.x, obj.p)
+ return obj
+
+def construct(tup):
+ r"""Construct an ElGamal key from a tuple of valid ElGamal components.
+
+ The modulus *p* must be a prime.
+ The following conditions must apply:
+
+ .. math::
+
+ \begin{align}
+ &1 < g < p-1 \\
+ &g^{p-1} = 1 \text{ mod } 1 \\
+ &1 < x < p-1 \\
+ &g^x = y \text{ mod } p
+ \end{align}
+
+ Args:
+ tup (tuple):
+ A tuple with either 3 or 4 integers,
+ in the following order:
+
+ 1. Modulus (*p*).
+ 2. Generator (*g*).
+ 3. Public key (*y*).
+ 4. Private key (*x*). Optional.
+
+ Raises:
+ ValueError: when the key being imported fails the most basic ElGamal validity checks.
+
+ Returns:
+ an :class:`ElGamalKey` object
+ """
+
+ obj=ElGamalKey()
+ if len(tup) not in [3,4]:
+ raise ValueError('argument for construct() wrong length')
+ for i in range(len(tup)):
+ field = obj._keydata[i]
+ setattr(obj, field, Integer(tup[i]))
+
+ fmt_error = test_probable_prime(obj.p) == COMPOSITE
+ fmt_error |= obj.g<=1 or obj.g>=obj.p
+ fmt_error |= pow(obj.g, obj.p-1, obj.p)!=1
+ fmt_error |= obj.y<1 or obj.y>=obj.p
+ if len(tup)==4:
+ fmt_error |= obj.x<=1 or obj.x>=obj.p
+ fmt_error |= pow(obj.g, obj.x, obj.p)!=obj.y
+
+ if fmt_error:
+ raise ValueError("Invalid ElGamal key components")
+
+ return obj
+
+class ElGamalKey(object):
+ r"""Class defining an ElGamal key.
+ Do not instantiate directly.
+ Use :func:`generate` or :func:`construct` instead.
+
+ :ivar p: Modulus
+ :vartype d: integer
+
+ :ivar g: Generator
+ :vartype e: integer
+
+ :ivar y: Public key component
+ :vartype y: integer
+
+ :ivar x: Private key component
+ :vartype x: integer
+ """
+
+ #: Dictionary of ElGamal parameters.
+ #:
+ #: A public key will only have the following entries:
+ #:
+ #: - **y**, the public key.
+ #: - **g**, the generator.
+ #: - **p**, the modulus.
+ #:
+ #: A private key will also have:
+ #:
+ #: - **x**, the private key.
+ _keydata=['p', 'g', 'y', 'x']
+
+ def __init__(self, randfunc=None):
+ if randfunc is None:
+ randfunc = Random.new().read
+ self._randfunc = randfunc
+
+ def _encrypt(self, M, K):
+ a=pow(self.g, K, self.p)
+ b=( pow(self.y, K, self.p)*M ) % self.p
+ return [int(a), int(b)]
+
+ def _decrypt(self, M):
+ if (not hasattr(self, 'x')):
+ raise TypeError('Private key not available in this object')
+ r = Integer.random_range(min_inclusive=2,
+ max_exclusive=self.p-1,
+ randfunc=self._randfunc)
+ a_blind = (pow(self.g, r, self.p) * M[0]) % self.p
+ ax=pow(a_blind, self.x, self.p)
+ plaintext_blind = (ax.inverse(self.p) * M[1] ) % self.p
+ plaintext = (plaintext_blind * pow(self.y, r, self.p)) % self.p
+ return int(plaintext)
+
+ def _sign(self, M, K):
+ if (not hasattr(self, 'x')):
+ raise TypeError('Private key not available in this object')
+ p1=self.p-1
+ K = Integer(K)
+ if (K.gcd(p1)!=1):
+ raise ValueError('Bad K value: GCD(K,p-1)!=1')
+ a=pow(self.g, K, self.p)
+ t=(Integer(M)-self.x*a) % p1
+ while t<0: t=t+p1
+ b=(t*K.inverse(p1)) % p1
+ return [int(a), int(b)]
+
+ def _verify(self, M, sig):
+ sig = [Integer(x) for x in sig]
+ if sig[0]<1 or sig[0]>self.p-1:
+ return 0
+ v1=pow(self.y, sig[0], self.p)
+ v1=(v1*pow(sig[0], sig[1], self.p)) % self.p
+ v2=pow(self.g, M, self.p)
+ if v1==v2:
+ return 1
+ return 0
+
+ def has_private(self):
+ """Whether this is an ElGamal private key"""
+
+ if hasattr(self, 'x'):
+ return 1
+ else:
+ return 0
+
+ def can_encrypt(self):
+ return True
+
+ def can_sign(self):
+ return True
+
+ def publickey(self):
+ """A matching ElGamal public key.
+
+ Returns:
+ a new :class:`ElGamalKey` object
+ """
+ return construct((self.p, self.g, self.y))
+
+ def __eq__(self, other):
+ if bool(self.has_private()) != bool(other.has_private()):
+ return False
+
+ result = True
+ for comp in self._keydata:
+ result = result and (getattr(self.key, comp, None) ==
+ getattr(other.key, comp, None))
+ return result
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __getstate__(self):
+ # ElGamal key is not pickable
+ from pickle import PicklingError
+ raise PicklingError
+
+ # Methods defined in PyCrypto that we don't support anymore
+
+ def sign(self, M, K):
+ raise NotImplementedError
+
+ def verify(self, M, signature):
+ raise NotImplementedError
+
+ def encrypt(self, plaintext, K):
+ raise NotImplementedError
+
+ def decrypt(self, ciphertext):
+ raise NotImplementedError
+
+ def blind(self, M, B):
+ raise NotImplementedError
+
+ def unblind(self, M, B):
+ raise NotImplementedError
+
+ def size(self):
+ raise NotImplementedError
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ElGamal.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ElGamal.pyi
new file mode 100644
index 00000000..90485313
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/ElGamal.pyi
@@ -0,0 +1,18 @@
+from typing import Callable, Union, Tuple, Optional
+
+__all__ = ['generate', 'construct', 'ElGamalKey']
+
+RNG = Callable[[int], bytes]
+
+def generate(bits: int, randfunc: RNG) -> ElGamalKey: ...
+def construct(tup: Union[Tuple[int, int, int], Tuple[int, int, int, int]]) -> ElGamalKey: ...
+
+class ElGamalKey(object):
+ def __init__(self, randfunc: Optional[RNG]=None) -> None: ...
+ def has_private(self) -> bool: ...
+ def can_encrypt(self) -> bool: ...
+ def can_sign(self) -> bool: ...
+ def publickey(self) -> ElGamalKey: ...
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
+ def __getstate__(self) -> None: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/RSA.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/RSA.py
new file mode 100644
index 00000000..bc8f9d2d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/RSA.py
@@ -0,0 +1,871 @@
+# -*- coding: utf-8 -*-
+# ===================================================================
+#
+# Copyright (c) 2016, Legrandin
+# 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.
+# ===================================================================
+
+__all__ = ['generate', 'construct', 'import_key',
+ 'RsaKey', 'oid']
+
+import binascii
+import struct
+
+from Crypto import Random
+from Crypto.Util.py3compat import tobytes, bord, tostr
+from Crypto.Util.asn1 import DerSequence, DerNull
+from Crypto.Util.number import bytes_to_long
+
+from Crypto.Math.Numbers import Integer
+from Crypto.Math.Primality import (test_probable_prime,
+ generate_probable_prime, COMPOSITE)
+
+from Crypto.PublicKey import (_expand_subject_public_key_info,
+ _create_subject_public_key_info,
+ _extract_subject_public_key_info)
+
+
+class RsaKey(object):
+ r"""Class defining an RSA key, private or public.
+ Do not instantiate directly.
+ Use :func:`generate`, :func:`construct` or :func:`import_key` instead.
+
+ :ivar n: RSA modulus
+ :vartype n: integer
+
+ :ivar e: RSA public exponent
+ :vartype e: integer
+
+ :ivar d: RSA private exponent
+ :vartype d: integer
+
+ :ivar p: First factor of the RSA modulus
+ :vartype p: integer
+
+ :ivar q: Second factor of the RSA modulus
+ :vartype q: integer
+
+ :ivar invp: Chinese remainder component (:math:`p^{-1} \text{mod } q`)
+ :vartype invp: integer
+
+ :ivar invq: Chinese remainder component (:math:`q^{-1} \text{mod } p`)
+ :vartype invq: integer
+
+ :ivar u: Same as ``invp``
+ :vartype u: integer
+ """
+
+ def __init__(self, **kwargs):
+ """Build an RSA key.
+
+ :Keywords:
+ n : integer
+ The modulus.
+ e : integer
+ The public exponent.
+ d : integer
+ The private exponent. Only required for private keys.
+ p : integer
+ The first factor of the modulus. Only required for private keys.
+ q : integer
+ The second factor of the modulus. Only required for private keys.
+ u : integer
+ The CRT coefficient (inverse of p modulo q). Only required for
+ private keys.
+ """
+
+ input_set = set(kwargs.keys())
+ public_set = set(('n', 'e'))
+ private_set = public_set | set(('p', 'q', 'd', 'u'))
+ if input_set not in (private_set, public_set):
+ raise ValueError("Some RSA components are missing")
+ for component, value in kwargs.items():
+ setattr(self, "_" + component, value)
+ if input_set == private_set:
+ self._dp = self._d % (self._p - 1) # = (e⁻¹) mod (p-1)
+ self._dq = self._d % (self._q - 1) # = (e⁻¹) mod (q-1)
+ self._invq = None # will be computed on demand
+
+ @property
+ def n(self):
+ return int(self._n)
+
+ @property
+ def e(self):
+ return int(self._e)
+
+ @property
+ def d(self):
+ if not self.has_private():
+ raise AttributeError("No private exponent available for public keys")
+ return int(self._d)
+
+ @property
+ def p(self):
+ if not self.has_private():
+ raise AttributeError("No CRT component 'p' available for public keys")
+ return int(self._p)
+
+ @property
+ def q(self):
+ if not self.has_private():
+ raise AttributeError("No CRT component 'q' available for public keys")
+ return int(self._q)
+
+ @property
+ def dp(self):
+ if not self.has_private():
+ raise AttributeError("No CRT component 'dp' available for public keys")
+ return int(self._dp)
+
+ @property
+ def dq(self):
+ if not self.has_private():
+ raise AttributeError("No CRT component 'dq' available for public keys")
+ return int(self._dq)
+
+ @property
+ def invq(self):
+ if not self.has_private():
+ raise AttributeError("No CRT component 'invq' available for public keys")
+ if self._invq is None:
+ self._invq = self._q.inverse(self._p)
+ return int(self._invq)
+
+ @property
+ def invp(self):
+ return self.u
+
+ @property
+ def u(self):
+ if not self.has_private():
+ raise AttributeError("No CRT component 'u' available for public keys")
+ return int(self._u)
+
+ def size_in_bits(self):
+ """Size of the RSA modulus in bits"""
+ return self._n.size_in_bits()
+
+ def size_in_bytes(self):
+ """The minimal amount of bytes that can hold the RSA modulus"""
+ return (self._n.size_in_bits() - 1) // 8 + 1
+
+ def _encrypt(self, plaintext):
+ if not 0 <= plaintext < self._n:
+ raise ValueError("Plaintext too large")
+ return int(pow(Integer(plaintext), self._e, self._n))
+
+ def _decrypt_to_bytes(self, ciphertext):
+ if not 0 <= ciphertext < self._n:
+ raise ValueError("Ciphertext too large")
+ if not self.has_private():
+ raise TypeError("This is not a private key")
+
+ # Blinded RSA decryption (to prevent timing attacks):
+ # Step 1: Generate random secret blinding factor r,
+ # such that 0 < r < n-1
+ r = Integer.random_range(min_inclusive=1, max_exclusive=self._n)
+ # Step 2: Compute c' = c * r**e mod n
+ cp = Integer(ciphertext) * pow(r, self._e, self._n) % self._n
+ # Step 3: Compute m' = c'**d mod n (normal RSA decryption)
+ m1 = pow(cp, self._dp, self._p)
+ m2 = pow(cp, self._dq, self._q)
+ h = ((m2 - m1) * self._u) % self._q
+ mp = h * self._p + m1
+ # Step 4: Compute m = m' * (r**(-1)) mod n
+ # then encode into a big endian byte string
+ result = Integer._mult_modulo_bytes(
+ r.inverse(self._n),
+ mp,
+ self._n)
+ return result
+
+ def _decrypt(self, ciphertext):
+ """Legacy private method"""
+
+ return bytes_to_long(self._decrypt_to_bytes(ciphertext))
+
+ def has_private(self):
+ """Whether this is an RSA private key"""
+
+ return hasattr(self, "_d")
+
+ def can_encrypt(self): # legacy
+ return True
+
+ def can_sign(self): # legacy
+ return True
+
+ def public_key(self):
+ """A matching RSA public key.
+
+ Returns:
+ a new :class:`RsaKey` object
+ """
+ return RsaKey(n=self._n, e=self._e)
+
+ def __eq__(self, other):
+ if self.has_private() != other.has_private():
+ return False
+ if self.n != other.n or self.e != other.e:
+ return False
+ if not self.has_private():
+ return True
+ return (self.d == other.d)
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __getstate__(self):
+ # RSA key is not pickable
+ from pickle import PicklingError
+ raise PicklingError
+
+ def __repr__(self):
+ if self.has_private():
+ extra = ", d=%d, p=%d, q=%d, u=%d" % (int(self._d), int(self._p),
+ int(self._q), int(self._u))
+ else:
+ extra = ""
+ return "RsaKey(n=%d, e=%d%s)" % (int(self._n), int(self._e), extra)
+
+ def __str__(self):
+ if self.has_private():
+ key_type = "Private"
+ else:
+ key_type = "Public"
+ return "%s RSA key at 0x%X" % (key_type, id(self))
+
+ def export_key(self, format='PEM', passphrase=None, pkcs=1,
+ protection=None, randfunc=None, prot_params=None):
+ """Export this RSA key.
+
+ Keyword Args:
+ format (string):
+ The desired output format:
+
+ - ``'PEM'``. (default) Text output, according to `RFC1421`_/`RFC1423`_.
+ - ``'DER'``. Binary output.
+ - ``'OpenSSH'``. Text output, according to the OpenSSH specification.
+ Only suitable for public keys (not private keys).
+
+ Note that PEM contains a DER structure.
+
+ passphrase (bytes or string):
+ (*Private keys only*) The passphrase to protect the
+ private key.
+
+ pkcs (integer):
+ (*Private keys only*) The standard to use for
+ serializing the key: PKCS#1 or PKCS#8.
+
+ With ``pkcs=1`` (*default*), the private key is encoded with a
+ simple `PKCS#1`_ structure (``RSAPrivateKey``). The key cannot be
+ securely encrypted.
+
+ With ``pkcs=8``, the private key is encoded with a `PKCS#8`_ structure
+ (``PrivateKeyInfo``). PKCS#8 offers the best ways to securely
+ encrypt the key.
+
+ .. note::
+ This parameter is ignored for a public key.
+ For DER and PEM, the output is always an
+ ASN.1 DER ``SubjectPublicKeyInfo`` structure.
+
+ protection (string):
+ (*For private keys only*)
+ The encryption scheme to use for protecting the private key
+ using the passphrase.
+
+ You can only specify a value if ``pkcs=8``.
+ For all possible protection schemes,
+ refer to :ref:`the encryption parameters of PKCS#8`.
+ The recommended value is
+ ``'PBKDF2WithHMAC-SHA512AndAES256-CBC'``.
+
+ If ``None`` (default), the behavior depends on :attr:`format`:
+
+ - if ``format='PEM'``, the obsolete PEM encryption scheme is used.
+ It is based on MD5 for key derivation, and 3DES for encryption.
+
+ - if ``format='DER'``, the ``'PBKDF2WithHMAC-SHA1AndDES-EDE3-CBC'``
+ scheme is used.
+
+ prot_params (dict):
+ (*For private keys only*)
+
+ The parameters to use to derive the encryption key
+ from the passphrase. ``'protection'`` must be also specified.
+ For all possible values,
+ refer to :ref:`the encryption parameters of PKCS#8`.
+ The recommendation is to use ``{'iteration_count':21000}`` for PBKDF2,
+ and ``{'iteration_count':131072}`` for scrypt.
+
+ randfunc (callable):
+ A function that provides random bytes. Only used for PEM encoding.
+ The default is :func:`Crypto.Random.get_random_bytes`.
+
+ Returns:
+ bytes: the encoded key
+
+ Raises:
+ ValueError:when the format is unknown or when you try to encrypt a private
+ key with *DER* format and PKCS#1.
+
+ .. warning::
+ If you don't provide a pass phrase, the private key will be
+ exported in the clear!
+
+ .. _RFC1421: http://www.ietf.org/rfc/rfc1421.txt
+ .. _RFC1423: http://www.ietf.org/rfc/rfc1423.txt
+ .. _`PKCS#1`: http://www.ietf.org/rfc/rfc3447.txt
+ .. _`PKCS#8`: http://www.ietf.org/rfc/rfc5208.txt
+ """
+
+ if passphrase is not None:
+ passphrase = tobytes(passphrase)
+
+ if randfunc is None:
+ randfunc = Random.get_random_bytes
+
+ if format == 'OpenSSH':
+ e_bytes, n_bytes = [x.to_bytes() for x in (self._e, self._n)]
+ if bord(e_bytes[0]) & 0x80:
+ e_bytes = b'\x00' + e_bytes
+ if bord(n_bytes[0]) & 0x80:
+ n_bytes = b'\x00' + n_bytes
+ keyparts = [b'ssh-rsa', e_bytes, n_bytes]
+ keystring = b''.join([struct.pack(">I", len(kp)) + kp for kp in keyparts])
+ return b'ssh-rsa ' + binascii.b2a_base64(keystring)[:-1]
+
+ # DER format is always used, even in case of PEM, which simply
+ # encodes it into BASE64.
+ if self.has_private():
+ binary_key = DerSequence([0,
+ self.n,
+ self.e,
+ self.d,
+ self.p,
+ self.q,
+ self.d % (self.p-1),
+ self.d % (self.q-1),
+ Integer(self.q).inverse(self.p)
+ ]).encode()
+ if pkcs == 1:
+ key_type = 'RSA PRIVATE KEY'
+ if format == 'DER' and passphrase:
+ raise ValueError("PKCS#1 private key cannot be encrypted")
+ else: # PKCS#8
+ from Crypto.IO import PKCS8
+
+ if format == 'PEM' and protection is None:
+ key_type = 'PRIVATE KEY'
+ binary_key = PKCS8.wrap(binary_key, oid, None,
+ key_params=DerNull())
+ else:
+ key_type = 'ENCRYPTED PRIVATE KEY'
+ if not protection:
+ if prot_params:
+ raise ValueError("'protection' parameter must be set")
+ protection = 'PBKDF2WithHMAC-SHA1AndDES-EDE3-CBC'
+ binary_key = PKCS8.wrap(binary_key, oid,
+ passphrase, protection,
+ prot_params=prot_params,
+ key_params=DerNull())
+ passphrase = None
+ else:
+ key_type = "PUBLIC KEY"
+ binary_key = _create_subject_public_key_info(oid,
+ DerSequence([self.n,
+ self.e]),
+ DerNull()
+ )
+
+ if format == 'DER':
+ return binary_key
+ if format == 'PEM':
+ from Crypto.IO import PEM
+
+ pem_str = PEM.encode(binary_key, key_type, passphrase, randfunc)
+ return tobytes(pem_str)
+
+ raise ValueError("Unknown key format '%s'. Cannot export the RSA key." % format)
+
+ # Backward compatibility
+ def exportKey(self, *args, **kwargs):
+ """:meta private:"""
+ return self.export_key(*args, **kwargs)
+
+ def publickey(self):
+ """:meta private:"""
+ return self.public_key()
+
+ # Methods defined in PyCrypto that we don't support anymore
+ def sign(self, M, K):
+ """:meta private:"""
+ raise NotImplementedError("Use module Crypto.Signature.pkcs1_15 instead")
+
+ def verify(self, M, signature):
+ """:meta private:"""
+ raise NotImplementedError("Use module Crypto.Signature.pkcs1_15 instead")
+
+ def encrypt(self, plaintext, K):
+ """:meta private:"""
+ raise NotImplementedError("Use module Crypto.Cipher.PKCS1_OAEP instead")
+
+ def decrypt(self, ciphertext):
+ """:meta private:"""
+ raise NotImplementedError("Use module Crypto.Cipher.PKCS1_OAEP instead")
+
+ def blind(self, M, B):
+ """:meta private:"""
+ raise NotImplementedError
+
+ def unblind(self, M, B):
+ """:meta private:"""
+ raise NotImplementedError
+
+ def size(self):
+ """:meta private:"""
+ raise NotImplementedError
+
+
+def generate(bits, randfunc=None, e=65537):
+ """Create a new RSA key pair.
+
+ The algorithm closely follows NIST `FIPS 186-4`_ in its
+ sections B.3.1 and B.3.3. The modulus is the product of
+ two non-strong probable primes.
+ Each prime passes a suitable number of Miller-Rabin tests
+ with random bases and a single Lucas test.
+
+ Args:
+ bits (integer):
+ Key length, or size (in bits) of the RSA modulus.
+ It must be at least 1024, but **2048 is recommended.**
+ The FIPS standard only defines 1024, 2048 and 3072.
+ Keyword Args:
+ randfunc (callable):
+ Function that returns random bytes.
+ The default is :func:`Crypto.Random.get_random_bytes`.
+ e (integer):
+ Public RSA exponent. It must be an odd positive integer.
+ It is typically a small number with very few ones in its
+ binary representation.
+ The FIPS standard requires the public exponent to be
+ at least 65537 (the default).
+
+ Returns: an RSA key object (:class:`RsaKey`, with private key).
+
+ .. _FIPS 186-4: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
+ """
+
+ if bits < 1024:
+ raise ValueError("RSA modulus length must be >= 1024")
+ if e % 2 == 0 or e < 3:
+ raise ValueError("RSA public exponent must be a positive, odd integer larger than 2.")
+
+ if randfunc is None:
+ randfunc = Random.get_random_bytes
+
+ d = n = Integer(1)
+ e = Integer(e)
+
+ while n.size_in_bits() != bits and d < (1 << (bits // 2)):
+ # Generate the prime factors of n: p and q.
+ # By construciton, their product is always
+ # 2^{bits-1} < p*q < 2^bits.
+ size_q = bits // 2
+ size_p = bits - size_q
+
+ min_p = min_q = (Integer(1) << (2 * size_q - 1)).sqrt()
+ if size_q != size_p:
+ min_p = (Integer(1) << (2 * size_p - 1)).sqrt()
+
+ def filter_p(candidate):
+ return candidate > min_p and (candidate - 1).gcd(e) == 1
+
+ p = generate_probable_prime(exact_bits=size_p,
+ randfunc=randfunc,
+ prime_filter=filter_p)
+
+ min_distance = Integer(1) << (bits // 2 - 100)
+
+ def filter_q(candidate):
+ return (candidate > min_q and
+ (candidate - 1).gcd(e) == 1 and
+ abs(candidate - p) > min_distance)
+
+ q = generate_probable_prime(exact_bits=size_q,
+ randfunc=randfunc,
+ prime_filter=filter_q)
+
+ n = p * q
+ lcm = (p - 1).lcm(q - 1)
+ d = e.inverse(lcm)
+
+ if p > q:
+ p, q = q, p
+
+ u = p.inverse(q)
+
+ return RsaKey(n=n, e=e, d=d, p=p, q=q, u=u)
+
+
+def construct(rsa_components, consistency_check=True):
+ r"""Construct an RSA key from a tuple of valid RSA components.
+
+ The modulus **n** must be the product of two primes.
+ The public exponent **e** must be odd and larger than 1.
+
+ In case of a private key, the following equations must apply:
+
+ .. math::
+
+ \begin{align}
+ p*q &= n \\
+ e*d &\equiv 1 ( \text{mod lcm} [(p-1)(q-1)]) \\
+ p*u &\equiv 1 ( \text{mod } q)
+ \end{align}
+
+ Args:
+ rsa_components (tuple):
+ A tuple of integers, with at least 2 and no
+ more than 6 items. The items come in the following order:
+
+ 1. RSA modulus *n*.
+ 2. Public exponent *e*.
+ 3. Private exponent *d*.
+ Only required if the key is private.
+ 4. First factor of *n* (*p*).
+ Optional, but the other factor *q* must also be present.
+ 5. Second factor of *n* (*q*). Optional.
+ 6. CRT coefficient *q*, that is :math:`p^{-1} \text{mod }q`. Optional.
+
+ Keyword Args:
+ consistency_check (boolean):
+ If ``True``, the library will verify that the provided components
+ fulfil the main RSA properties.
+
+ Raises:
+ ValueError: when the key being imported fails the most basic RSA validity checks.
+
+ Returns: An RSA key object (:class:`RsaKey`).
+ """
+
+ class InputComps(object):
+ pass
+
+ input_comps = InputComps()
+ for (comp, value) in zip(('n', 'e', 'd', 'p', 'q', 'u'), rsa_components):
+ setattr(input_comps, comp, Integer(value))
+
+ n = input_comps.n
+ e = input_comps.e
+ if not hasattr(input_comps, 'd'):
+ key = RsaKey(n=n, e=e)
+ else:
+ d = input_comps.d
+ if hasattr(input_comps, 'q'):
+ p = input_comps.p
+ q = input_comps.q
+ else:
+ # Compute factors p and q from the private exponent d.
+ # We assume that n has no more than two factors.
+ # See 8.2.2(i) in Handbook of Applied Cryptography.
+ ktot = d * e - 1
+ # The quantity d*e-1 is a multiple of phi(n), even,
+ # and can be represented as t*2^s.
+ t = ktot
+ while t % 2 == 0:
+ t //= 2
+ # Cycle through all multiplicative inverses in Zn.
+ # The algorithm is non-deterministic, but there is a 50% chance
+ # any candidate a leads to successful factoring.
+ # See "Digitalized Signatures and Public Key Functions as Intractable
+ # as Factorization", M. Rabin, 1979
+ spotted = False
+ a = Integer(2)
+ while not spotted and a < 100:
+ k = Integer(t)
+ # Cycle through all values a^{t*2^i}=a^k
+ while k < ktot:
+ cand = pow(a, k, n)
+ # Check if a^k is a non-trivial root of unity (mod n)
+ if cand != 1 and cand != (n - 1) and pow(cand, 2, n) == 1:
+ # We have found a number such that (cand-1)(cand+1)=0 (mod n).
+ # Either of the terms divides n.
+ p = Integer(n).gcd(cand + 1)
+ spotted = True
+ break
+ k *= 2
+ # This value was not any good... let's try another!
+ a += 2
+ if not spotted:
+ raise ValueError("Unable to compute factors p and q from exponent d.")
+ # Found !
+ assert ((n % p) == 0)
+ q = n // p
+
+ if hasattr(input_comps, 'u'):
+ u = input_comps.u
+ else:
+ u = p.inverse(q)
+
+ # Build key object
+ key = RsaKey(n=n, e=e, d=d, p=p, q=q, u=u)
+
+ # Verify consistency of the key
+ if consistency_check:
+
+ # Modulus and public exponent must be coprime
+ if e <= 1 or e >= n:
+ raise ValueError("Invalid RSA public exponent")
+ if Integer(n).gcd(e) != 1:
+ raise ValueError("RSA public exponent is not coprime to modulus")
+
+ # For RSA, modulus must be odd
+ if not n & 1:
+ raise ValueError("RSA modulus is not odd")
+
+ if key.has_private():
+ # Modulus and private exponent must be coprime
+ if d <= 1 or d >= n:
+ raise ValueError("Invalid RSA private exponent")
+ if Integer(n).gcd(d) != 1:
+ raise ValueError("RSA private exponent is not coprime to modulus")
+ # Modulus must be product of 2 primes
+ if p * q != n:
+ raise ValueError("RSA factors do not match modulus")
+ if test_probable_prime(p) == COMPOSITE:
+ raise ValueError("RSA factor p is composite")
+ if test_probable_prime(q) == COMPOSITE:
+ raise ValueError("RSA factor q is composite")
+ # See Carmichael theorem
+ phi = (p - 1) * (q - 1)
+ lcm = phi // (p - 1).gcd(q - 1)
+ if (e * d % int(lcm)) != 1:
+ raise ValueError("Invalid RSA condition")
+ if hasattr(key, 'u'):
+ # CRT coefficient
+ if u <= 1 or u >= q:
+ raise ValueError("Invalid RSA component u")
+ if (p * u % q) != 1:
+ raise ValueError("Invalid RSA component u with p")
+
+ return key
+
+
+def _import_pkcs1_private(encoded, *kwargs):
+ # RSAPrivateKey ::= SEQUENCE {
+ # version Version,
+ # modulus INTEGER, -- n
+ # publicExponent INTEGER, -- e
+ # privateExponent INTEGER, -- d
+ # prime1 INTEGER, -- p
+ # prime2 INTEGER, -- q
+ # exponent1 INTEGER, -- d mod (p-1)
+ # exponent2 INTEGER, -- d mod (q-1)
+ # coefficient INTEGER -- (inverse of q) mod p
+ # }
+ #
+ # Version ::= INTEGER
+ der = DerSequence().decode(encoded, nr_elements=9, only_ints_expected=True)
+ if der[0] != 0:
+ raise ValueError("No PKCS#1 encoding of an RSA private key")
+ return construct(der[1:6] + [Integer(der[4]).inverse(der[5])])
+
+
+def _import_pkcs1_public(encoded, *kwargs):
+ # RSAPublicKey ::= SEQUENCE {
+ # modulus INTEGER, -- n
+ # publicExponent INTEGER -- e
+ # }
+ der = DerSequence().decode(encoded, nr_elements=2, only_ints_expected=True)
+ return construct(der)
+
+
+def _import_subjectPublicKeyInfo(encoded, *kwargs):
+
+ oids = (oid, "1.2.840.113549.1.1.10")
+
+ algoid, encoded_key, params = _expand_subject_public_key_info(encoded)
+ if algoid not in oids or params is not None:
+ raise ValueError("No RSA subjectPublicKeyInfo")
+ return _import_pkcs1_public(encoded_key)
+
+
+def _import_x509_cert(encoded, *kwargs):
+
+ sp_info = _extract_subject_public_key_info(encoded)
+ return _import_subjectPublicKeyInfo(sp_info)
+
+
+def _import_pkcs8(encoded, passphrase):
+ from Crypto.IO import PKCS8
+
+ oids = (oid, "1.2.840.113549.1.1.10")
+
+ k = PKCS8.unwrap(encoded, passphrase)
+ if k[0] not in oids:
+ raise ValueError("No PKCS#8 encoded RSA key")
+ return _import_keyDER(k[1], passphrase)
+
+
+def _import_keyDER(extern_key, passphrase):
+ """Import an RSA key (public or private half), encoded in DER form."""
+
+ decodings = (_import_pkcs1_private,
+ _import_pkcs1_public,
+ _import_subjectPublicKeyInfo,
+ _import_x509_cert,
+ _import_pkcs8)
+
+ for decoding in decodings:
+ try:
+ return decoding(extern_key, passphrase)
+ except ValueError:
+ pass
+
+ raise ValueError("RSA key format is not supported")
+
+
+def _import_openssh_private_rsa(data, password):
+
+ from ._openssh import (import_openssh_private_generic,
+ read_bytes, read_string, check_padding)
+
+ ssh_name, decrypted = import_openssh_private_generic(data, password)
+
+ if ssh_name != "ssh-rsa":
+ raise ValueError("This SSH key is not RSA")
+
+ n, decrypted = read_bytes(decrypted)
+ e, decrypted = read_bytes(decrypted)
+ d, decrypted = read_bytes(decrypted)
+ iqmp, decrypted = read_bytes(decrypted)
+ p, decrypted = read_bytes(decrypted)
+ q, decrypted = read_bytes(decrypted)
+
+ _, padded = read_string(decrypted) # Comment
+ check_padding(padded)
+
+ build = [Integer.from_bytes(x) for x in (n, e, d, q, p, iqmp)]
+ return construct(build)
+
+
+def import_key(extern_key, passphrase=None):
+ """Import an RSA key (public or private).
+
+ Args:
+ extern_key (string or byte string):
+ The RSA key to import.
+
+ The following formats are supported for an RSA **public key**:
+
+ - X.509 certificate (binary or PEM format)
+ - X.509 ``subjectPublicKeyInfo`` DER SEQUENCE (binary or PEM
+ encoding)
+ - `PKCS#1`_ ``RSAPublicKey`` DER SEQUENCE (binary or PEM encoding)
+ - An OpenSSH line (e.g. the content of ``~/.ssh/id_ecdsa``, ASCII)
+
+ The following formats are supported for an RSA **private key**:
+
+ - PKCS#1 ``RSAPrivateKey`` DER SEQUENCE (binary or PEM encoding)
+ - `PKCS#8`_ ``PrivateKeyInfo`` or ``EncryptedPrivateKeyInfo``
+ DER SEQUENCE (binary or PEM encoding)
+ - OpenSSH (text format, introduced in `OpenSSH 6.5`_)
+
+ For details about the PEM encoding, see `RFC1421`_/`RFC1423`_.
+
+ passphrase (string or byte string):
+ For private keys only, the pass phrase that encrypts the key.
+
+ Returns: An RSA key object (:class:`RsaKey`).
+
+ Raises:
+ ValueError/IndexError/TypeError:
+ When the given key cannot be parsed (possibly because the pass
+ phrase is wrong).
+
+ .. _RFC1421: http://www.ietf.org/rfc/rfc1421.txt
+ .. _RFC1423: http://www.ietf.org/rfc/rfc1423.txt
+ .. _`PKCS#1`: http://www.ietf.org/rfc/rfc3447.txt
+ .. _`PKCS#8`: http://www.ietf.org/rfc/rfc5208.txt
+ .. _`OpenSSH 6.5`: https://flak.tedunangst.com/post/new-openssh-key-format-and-bcrypt-pbkdf
+ """
+
+ from Crypto.IO import PEM
+
+ extern_key = tobytes(extern_key)
+ if passphrase is not None:
+ passphrase = tobytes(passphrase)
+
+ if extern_key.startswith(b'-----BEGIN OPENSSH PRIVATE KEY'):
+ text_encoded = tostr(extern_key)
+ openssh_encoded, marker, enc_flag = PEM.decode(text_encoded, passphrase)
+ result = _import_openssh_private_rsa(openssh_encoded, passphrase)
+ return result
+
+ if extern_key.startswith(b'-----'):
+ # This is probably a PEM encoded key.
+ (der, marker, enc_flag) = PEM.decode(tostr(extern_key), passphrase)
+ if enc_flag:
+ passphrase = None
+ return _import_keyDER(der, passphrase)
+
+ if extern_key.startswith(b'ssh-rsa '):
+ # This is probably an OpenSSH key
+ keystring = binascii.a2b_base64(extern_key.split(b' ')[1])
+ keyparts = []
+ while len(keystring) > 4:
+ length = struct.unpack(">I", keystring[:4])[0]
+ keyparts.append(keystring[4:4 + length])
+ keystring = keystring[4 + length:]
+ e = Integer.from_bytes(keyparts[1])
+ n = Integer.from_bytes(keyparts[2])
+ return construct([n, e])
+
+ if len(extern_key) > 0 and bord(extern_key[0]) == 0x30:
+ # This is probably a DER encoded key
+ return _import_keyDER(extern_key, passphrase)
+
+ raise ValueError("RSA key format is not supported")
+
+
+# Backward compatibility
+importKey = import_key
+
+#: `Object ID`_ for the RSA encryption algorithm. This OID often indicates
+#: a generic RSA key, even when such key will be actually used for digital
+#: signatures.
+#:
+#: .. note:
+#: An RSA key meant for PSS padding has a dedicated Object ID ``1.2.840.113549.1.1.10``
+#:
+#: .. _`Object ID`: http://www.alvestrand.no/objectid/1.2.840.113549.1.1.1.html
+oid = "1.2.840.113549.1.1.1"
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/RSA.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/RSA.pyi
new file mode 100644
index 00000000..9d3d1570
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/RSA.pyi
@@ -0,0 +1,78 @@
+from typing import Callable, Union, Tuple, Optional, overload, Literal
+
+from Crypto.Math.Numbers import Integer
+from Crypto.IO._PBES import ProtParams
+
+__all__ = ['generate', 'construct', 'import_key',
+ 'RsaKey', 'oid']
+
+RNG = Callable[[int], bytes]
+
+class RsaKey(object):
+ def __init__(self, **kwargs: int) -> None: ...
+
+ @property
+ def n(self) -> int: ...
+ @property
+ def e(self) -> int: ...
+ @property
+ def d(self) -> int: ...
+ @property
+ def p(self) -> int: ...
+ @property
+ def q(self) -> int: ...
+ @property
+ def u(self) -> int: ...
+ @property
+ def invp(self) -> int: ...
+ @property
+ def invq(self) -> int: ...
+
+ def size_in_bits(self) -> int: ...
+ def size_in_bytes(self) -> int: ...
+ def has_private(self) -> bool: ...
+ def can_encrypt(self) -> bool: ... # legacy
+ def can_sign(self) -> bool:... # legacy
+ def public_key(self) -> RsaKey: ...
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
+ def __getstate__(self) -> None: ...
+ def __repr__(self) -> str: ...
+ def __str__(self) -> str: ...
+
+ @overload
+ def export_key(self,
+ format: Optional[str]="PEM",
+ passphrase: Optional[str]=None,
+ pkcs: Optional[int]=1,
+ protection: Optional[str]=None,
+ randfunc: Optional[RNG]=None
+ ) -> bytes: ...
+ @overload
+ def export_key(self, *,
+ format: Optional[str]="PEM",
+ passphrase: str,
+ pkcs: Literal[8],
+ protection: str,
+ randfunc: Optional[RNG]=None,
+ prot_params: ProtParams,
+ ) -> bytes: ...
+
+ # Backward compatibility
+ exportKey = export_key
+ publickey = public_key
+
+Int = Union[int, Integer]
+
+def generate(bits: int, randfunc: Optional[RNG]=None, e: Optional[int]=65537) -> RsaKey: ...
+def construct(rsa_components: Union[Tuple[Int, Int], # n, e
+ Tuple[Int, Int, Int], # n, e, d
+ Tuple[Int, Int, Int, Int, Int], # n, e, d, p, q
+ Tuple[Int, Int, Int, Int, Int, Int]], # n, e, d, p, q, crt_q
+ consistency_check: Optional[bool]=True) -> RsaKey: ...
+def import_key(extern_key: Union[str, bytes], passphrase: Optional[str]=None) -> RsaKey: ...
+
+# Backward compatibility
+importKey = import_key
+
+oid: str
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/__init__.py
new file mode 100644
index 00000000..cf3a2384
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/__init__.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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.asn1 import (DerSequence, DerInteger, DerBitString,
+ DerObjectId, DerNull)
+
+
+def _expand_subject_public_key_info(encoded):
+ """Parse a SubjectPublicKeyInfo structure.
+
+ It returns a triple with:
+ * OID (string)
+ * encoded public key (bytes)
+ * Algorithm parameters (bytes or None)
+ """
+
+ #
+ # SubjectPublicKeyInfo ::= SEQUENCE {
+ # algorithm AlgorithmIdentifier,
+ # subjectPublicKey BIT STRING
+ # }
+ #
+ # AlgorithmIdentifier ::= SEQUENCE {
+ # algorithm OBJECT IDENTIFIER,
+ # parameters ANY DEFINED BY algorithm OPTIONAL
+ # }
+ #
+
+ spki = DerSequence().decode(encoded, nr_elements=2)
+ algo = DerSequence().decode(spki[0], nr_elements=(1,2))
+ algo_oid = DerObjectId().decode(algo[0])
+ spk = DerBitString().decode(spki[1]).value
+
+ if len(algo) == 1:
+ algo_params = None
+ else:
+ try:
+ DerNull().decode(algo[1])
+ algo_params = None
+ except:
+ algo_params = algo[1]
+
+ return algo_oid.value, spk, algo_params
+
+
+def _create_subject_public_key_info(algo_oid, public_key, params):
+
+ if params is None:
+ algorithm = DerSequence([DerObjectId(algo_oid)])
+ else:
+ algorithm = DerSequence([DerObjectId(algo_oid), params])
+
+ spki = DerSequence([algorithm,
+ DerBitString(public_key)
+ ])
+ return spki.encode()
+
+
+def _extract_subject_public_key_info(x509_certificate):
+ """Extract subjectPublicKeyInfo from a DER X.509 certificate."""
+
+ certificate = DerSequence().decode(x509_certificate, nr_elements=3)
+ tbs_certificate = DerSequence().decode(certificate[0],
+ nr_elements=range(6, 11))
+
+ index = 5
+ try:
+ tbs_certificate[0] + 1
+ # Version not present
+ version = 1
+ except TypeError:
+ version = DerInteger(explicit=0).decode(tbs_certificate[0]).value
+ if version not in (2, 3):
+ raise ValueError("Incorrect X.509 certificate version")
+ index = 6
+
+ return tbs_certificate[index]
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/__init__.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/__init__.pyi
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_curve.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_curve.py
new file mode 100644
index 00000000..0027f611
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_curve.py
@@ -0,0 +1,37 @@
+# This file is licensed under the BSD 2-Clause License.
+# See https://opensource.org/licenses/BSD-2-Clause for details.
+
+# This is the element of a database of curve parameters. Items are indexed by their
+# human-friendly name, such as "P-256". The element has the following fields:
+#
+# - p the prime number that defines the finite field for all modulo operations
+# - b the constant in the Short Weierstrass curve equation (can be None)
+# - order the number of elements in the group with the generator below
+# - Gx the affine coordinate X of the generator point
+# - Gy the affine coordinate Y of the generator point
+# - G the generator, as an EccPoint object
+# - modulus_bits the minimum number of bits for encoding the modulus p
+# - oid an ASCII string with the registered ASN.1 Object ID
+# - context a raw pointer to memory holding a context for all curve operations (can be None)
+# - canonical the canonical name of the curve
+# - openssh the ASCII string used in OpenSSH id files for public keys on this curve
+# - rawlib the reference to the dynamic libary with the low-level functions
+# - validate a function that raises an exception if the the input point is invalid
+
+class _Curve(object):
+
+ def __init__(self, p, b, order, Gx, Gy, G, modulus_bits, oid, context,
+ canonical, openssh, rawlib, validate=None):
+ self.p = p
+ self.b = b
+ self.order = order
+ self.Gx = Gx
+ self.Gy = Gy
+ self.G = G
+ self.modulus_bits = modulus_bits
+ self.oid = oid
+ self.context = context
+ self.canonical = canonical
+ self.openssh = openssh
+ self.rawlib = rawlib
+ self.validate = validate
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_curve25519.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_curve25519.abi3.so
new file mode 100755
index 00000000..16fa548a
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_curve25519.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_curve448.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_curve448.abi3.so
new file mode 100755
index 00000000..e0ff62b1
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_curve448.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_ec_ws.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_ec_ws.abi3.so
new file mode 100755
index 00000000..8f17395e
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_ec_ws.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_ed25519.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_ed25519.abi3.so
new file mode 100755
index 00000000..2ca53609
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_ed25519.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_ed448.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_ed448.abi3.so
new file mode 100755
index 00000000..64be8d83
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_ed448.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_edwards.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_edwards.py
new file mode 100644
index 00000000..5b6bc6ec
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_edwards.py
@@ -0,0 +1,116 @@
+# This file is licensed under the BSD 2-Clause License.
+# See https://opensource.org/licenses/BSD-2-Clause for details.
+
+from ._curve import _Curve
+from Crypto.Math.Numbers import Integer
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
+ SmartPointer)
+
+
+def ed25519_curve():
+ p = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed # 2**255 - 19
+ order = 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed
+ Gx = 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51a
+ Gy = 0x6666666666666666666666666666666666666666666666666666666666666658
+
+ _ed25519_lib = load_pycryptodome_raw_lib("Crypto.PublicKey._ed25519", """
+typedef void Point;
+int ed25519_new_point(Point **out,
+ const uint8_t x[32],
+ const uint8_t y[32],
+ size_t modsize,
+ const void *context);
+int ed25519_clone(Point **P, const Point *Q);
+void ed25519_free_point(Point *p);
+int ed25519_cmp(const Point *p1, const Point *p2);
+int ed25519_neg(Point *p);
+int ed25519_get_xy(uint8_t *xb, uint8_t *yb, size_t modsize, Point *p);
+int ed25519_double(Point *p);
+int ed25519_add(Point *P1, const Point *P2);
+int ed25519_scalar(Point *P, const uint8_t *scalar, size_t scalar_len, uint64_t seed);
+""")
+
+ class EcLib(object):
+ new_point = _ed25519_lib.ed25519_new_point
+ clone = _ed25519_lib.ed25519_clone
+ free_point = _ed25519_lib.ed25519_free_point
+ cmp = _ed25519_lib.ed25519_cmp
+ neg = _ed25519_lib.ed25519_neg
+ get_xy = _ed25519_lib.ed25519_get_xy
+ double = _ed25519_lib.ed25519_double
+ add = _ed25519_lib.ed25519_add
+ scalar = _ed25519_lib.ed25519_scalar
+
+ ed25519 = _Curve(Integer(p),
+ None,
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 255,
+ "1.3.101.112", # RFC8410
+ None,
+ "Ed25519",
+ "ssh-ed25519",
+ EcLib)
+ return ed25519
+
+
+def ed448_curve():
+ p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffff # 2**448 - 2**224 - 1
+ order = 0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffff7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3
+ Gx = 0x4f1970c66bed0ded221d15a622bf36da9e146570470f1767ea6de324a3d3a46412ae1af72ab66511433b80e18b00938e2626a82bc70cc05e
+ Gy = 0x693f46716eb6bc248876203756c9c7624bea73736ca3984087789c1e05a0c2d73ad3ff1ce67c39c4fdbd132c4ed7c8ad9808795bf230fa14
+
+ _ed448_lib = load_pycryptodome_raw_lib("Crypto.PublicKey._ed448", """
+typedef void EcContext;
+typedef void PointEd448;
+int ed448_new_context(EcContext **pec_ctx);
+void ed448_context(EcContext *ec_ctx);
+void ed448_free_context(EcContext *ec_ctx);
+int ed448_new_point(PointEd448 **out,
+ const uint8_t x[56],
+ const uint8_t y[56],
+ size_t len,
+ const EcContext *context);
+int ed448_clone(PointEd448 **P, const PointEd448 *Q);
+void ed448_free_point(PointEd448 *p);
+int ed448_cmp(const PointEd448 *p1, const PointEd448 *p2);
+int ed448_neg(PointEd448 *p);
+int ed448_get_xy(uint8_t *xb, uint8_t *yb, size_t len, const PointEd448 *p);
+int ed448_double(PointEd448 *p);
+int ed448_add(PointEd448 *P1, const PointEd448 *P2);
+int ed448_scalar(PointEd448 *P, const uint8_t *scalar, size_t scalar_len, uint64_t seed);
+""")
+
+ class EcLib(object):
+ new_point = _ed448_lib.ed448_new_point
+ clone = _ed448_lib.ed448_clone
+ free_point = _ed448_lib.ed448_free_point
+ cmp = _ed448_lib.ed448_cmp
+ neg = _ed448_lib.ed448_neg
+ get_xy = _ed448_lib.ed448_get_xy
+ double = _ed448_lib.ed448_double
+ add = _ed448_lib.ed448_add
+ scalar = _ed448_lib.ed448_scalar
+
+ ed448_context = VoidPointer()
+ result = _ed448_lib.ed448_new_context(ed448_context.address_of())
+ if result:
+ raise ImportError("Error %d initializing Ed448 context" % result)
+
+ context = SmartPointer(ed448_context.get(), _ed448_lib.ed448_free_context)
+
+ ed448 = _Curve(Integer(p),
+ None,
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 448,
+ "1.3.101.113", # RFC8410
+ context,
+ "Ed448",
+ None,
+ EcLib)
+ return ed448
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_montgomery.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_montgomery.py
new file mode 100644
index 00000000..bacc4ba4
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_montgomery.py
@@ -0,0 +1,152 @@
+# This file is licensed under the BSD 2-Clause License.
+# See https://opensource.org/licenses/BSD-2-Clause for details.
+
+from ._curve import _Curve
+from Crypto.Math.Numbers import Integer
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
+ SmartPointer)
+
+
+def curve25519_curve():
+ p = 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed # 2**255 - 19
+ order = 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed
+
+ _curve25519_lib = load_pycryptodome_raw_lib("Crypto.PublicKey._curve25519", """
+typedef void Point;
+
+int curve25519_new_point(Point **out,
+ const uint8_t x[32],
+ size_t modsize,
+ const void* context);
+int curve25519_clone(Point **P, const Point *Q);
+void curve25519_free_point(Point *p);
+int curve25519_get_x(uint8_t *xb, size_t modsize, Point *p);
+int curve25519_scalar(Point *P, const uint8_t *scalar, size_t scalar_len, uint64_t seed);
+int curve25519_cmp(const Point *ecp1, const Point *ecp2);
+""")
+
+ class EcLib(object):
+ new_point = _curve25519_lib.curve25519_new_point
+ clone = _curve25519_lib.curve25519_clone
+ free_point = _curve25519_lib.curve25519_free_point
+ get_x = _curve25519_lib.curve25519_get_x
+ scalar = _curve25519_lib.curve25519_scalar
+ cmp = _curve25519_lib.curve25519_cmp
+
+ def _validate_x25519_point(point):
+
+ p2 = p * 2
+ x1 = 325606250916557431795983626356110631294008115727848805560023387167927233504
+ x2 = 39382357235489614581723060781553021112529911719440698176882885853963445705823
+
+ # http://cr.yp.to/ecdh.html#validate
+ deny_list = (
+ 0,
+ 1,
+ x1,
+ x2,
+ p - 1,
+ p,
+ p + 1,
+ p + x1,
+ p + x2,
+ p2 - 1,
+ p2,
+ p2 + 1,
+ )
+
+ try:
+ valid = point.x not in deny_list
+ except ValueError:
+ valid = False
+
+ if not valid:
+ raise ValueError("Invalid Curve25519 public key")
+
+ curve25519 = _Curve(Integer(p),
+ None,
+ Integer(order),
+ Integer(9),
+ None,
+ None,
+ 255,
+ "1.3.101.110", # RFC8410
+ None,
+ "Curve25519",
+ None,
+ EcLib,
+ _validate_x25519_point,
+ )
+
+ return curve25519
+
+
+def curve448_curve():
+ p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffff # 2**448 - 2**224 - 1
+ order = 0x3fffffffffffffffffffffffffffffffffffffffffffffffffffffff7cca23e9c44edb49aed63690216cc2728dc58f552378c292ab5844f3
+
+ _curve448_lib = load_pycryptodome_raw_lib("Crypto.PublicKey._curve448", """
+typedef void Curve448Context;
+typedef void Curve448Point;
+
+int curve448_new_context(Curve448Context **pec_ctx);
+void curve448_free_context(Curve448Context *ec_ctx);
+int curve448_new_point(Curve448Point **out,
+ const uint8_t *x,
+ size_t len,
+ const Curve448Context *ec_ctx);
+void curve448_free_point(Curve448Point *p);
+int curve448_clone(Curve448Point **P, const Curve448Point *Q);
+int curve448_get_x(uint8_t *xb, size_t modsize, const Curve448Point *p);
+int curve448_scalar(Curve448Point *P, const uint8_t *scalar, size_t scalar_len, uint64_t seed);
+int curve448_cmp(const Curve448Point *ecp1, const Curve448Point *ecp2);
+""")
+
+ class EcLib(object):
+ new_context = _curve448_lib.curve448_new_context
+ free_context = _curve448_lib.curve448_free_context
+ new_point = _curve448_lib.curve448_new_point
+ clone = _curve448_lib.curve448_clone
+ free_point = _curve448_lib.curve448_free_point
+ get_x = _curve448_lib.curve448_get_x
+ scalar = _curve448_lib.curve448_scalar
+ cmp = _curve448_lib.curve448_cmp
+
+ curve448_context = VoidPointer()
+ result = EcLib.new_context(curve448_context.address_of())
+ if result:
+ raise ImportError("Error %d initializing Curve448 context" % result)
+
+ def _validate_x448_point(point):
+ deny_list = (
+ 0,
+ 1,
+ p - 1,
+ p,
+ p + 1,
+ )
+
+ try:
+ valid = point.x not in deny_list
+ except ValueError:
+ valid = False
+
+ if not valid:
+ raise ValueError("Invalid Curve448 public key")
+
+ curve448 = _Curve(Integer(p),
+ None,
+ Integer(order),
+ Integer(5),
+ None,
+ None,
+ 448,
+ "1.3.101.111", # RFC8410
+ SmartPointer(curve448_context.get(), EcLib.free_context),
+ "Curve448",
+ None,
+ EcLib,
+ _validate_x448_point,
+ )
+
+ return curve448
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_nist_ecc.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_nist_ecc.py
new file mode 100644
index 00000000..82c8bd58
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_nist_ecc.py
@@ -0,0 +1,246 @@
+# This file is licensed under the BSD 2-Clause License.
+# See https://opensource.org/licenses/BSD-2-Clause for details.
+
+from ._curve import _Curve
+from Crypto.Math.Numbers import Integer
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib, VoidPointer,
+ SmartPointer, c_size_t, c_uint8_ptr,
+ c_ulonglong)
+from Crypto.Util.number import long_to_bytes
+from Crypto.Random.random import getrandbits
+
+
+_ec_lib = load_pycryptodome_raw_lib("Crypto.PublicKey._ec_ws", """
+typedef void EcContext;
+typedef void EcPoint;
+int ec_ws_new_context(EcContext **pec_ctx,
+ const uint8_t *modulus,
+ const uint8_t *b,
+ const uint8_t *order,
+ size_t len,
+ uint64_t seed);
+void ec_ws_free_context(EcContext *ec_ctx);
+int ec_ws_new_point(EcPoint **pecp,
+ const uint8_t *x,
+ const uint8_t *y,
+ size_t len,
+ const EcContext *ec_ctx);
+void ec_ws_free_point(EcPoint *ecp);
+int ec_ws_get_xy(uint8_t *x,
+ uint8_t *y,
+ size_t len,
+ const EcPoint *ecp);
+int ec_ws_double(EcPoint *p);
+int ec_ws_add(EcPoint *ecpa, EcPoint *ecpb);
+int ec_ws_scalar(EcPoint *ecp,
+ const uint8_t *k,
+ size_t len,
+ uint64_t seed);
+int ec_ws_clone(EcPoint **pecp2, const EcPoint *ecp);
+int ec_ws_cmp(const EcPoint *ecp1, const EcPoint *ecp2);
+int ec_ws_neg(EcPoint *p);
+""")
+
+
+class EcLib(object):
+ new_context = _ec_lib.ec_ws_new_context
+ free_context = _ec_lib.ec_ws_free_context
+ new_point = _ec_lib.ec_ws_new_point
+ free_point = _ec_lib.ec_ws_free_point
+ get_xy = _ec_lib.ec_ws_get_xy
+ double = _ec_lib.ec_ws_double
+ add = _ec_lib.ec_ws_add
+ scalar = _ec_lib.ec_ws_scalar
+ clone = _ec_lib.ec_ws_clone
+ cmp = _ec_lib.ec_ws_cmp
+ neg = _ec_lib.ec_ws_neg
+
+
+def p192_curve():
+ p = 0xfffffffffffffffffffffffffffffffeffffffffffffffff
+ b = 0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1
+ order = 0xffffffffffffffffffffffff99def836146bc9b1b4d22831
+ Gx = 0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012
+ Gy = 0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811
+
+ p192_modulus = long_to_bytes(p, 24)
+ p192_b = long_to_bytes(b, 24)
+ p192_order = long_to_bytes(order, 24)
+
+ ec_p192_context = VoidPointer()
+ result = _ec_lib.ec_ws_new_context(ec_p192_context.address_of(),
+ c_uint8_ptr(p192_modulus),
+ c_uint8_ptr(p192_b),
+ c_uint8_ptr(p192_order),
+ c_size_t(len(p192_modulus)),
+ c_ulonglong(getrandbits(64))
+ )
+ if result:
+ raise ImportError("Error %d initializing P-192 context" % result)
+
+ context = SmartPointer(ec_p192_context.get(), _ec_lib.ec_ws_free_context)
+ p192 = _Curve(Integer(p),
+ Integer(b),
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 192,
+ "1.2.840.10045.3.1.1", # ANSI X9.62 / SEC2
+ context,
+ "NIST P-192",
+ "ecdsa-sha2-nistp192",
+ EcLib)
+ return p192
+
+
+def p224_curve():
+ p = 0xffffffffffffffffffffffffffffffff000000000000000000000001
+ b = 0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4
+ order = 0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d
+ Gx = 0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21
+ Gy = 0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34
+
+ p224_modulus = long_to_bytes(p, 28)
+ p224_b = long_to_bytes(b, 28)
+ p224_order = long_to_bytes(order, 28)
+
+ ec_p224_context = VoidPointer()
+ result = _ec_lib.ec_ws_new_context(ec_p224_context.address_of(),
+ c_uint8_ptr(p224_modulus),
+ c_uint8_ptr(p224_b),
+ c_uint8_ptr(p224_order),
+ c_size_t(len(p224_modulus)),
+ c_ulonglong(getrandbits(64))
+ )
+ if result:
+ raise ImportError("Error %d initializing P-224 context" % result)
+
+ context = SmartPointer(ec_p224_context.get(), _ec_lib.ec_ws_free_context)
+ p224 = _Curve(Integer(p),
+ Integer(b),
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 224,
+ "1.3.132.0.33", # SEC 2
+ context,
+ "NIST P-224",
+ "ecdsa-sha2-nistp224",
+ EcLib)
+ return p224
+
+
+def p256_curve():
+ p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff
+ b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
+ order = 0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551
+ Gx = 0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296
+ Gy = 0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5
+
+ p256_modulus = long_to_bytes(p, 32)
+ p256_b = long_to_bytes(b, 32)
+ p256_order = long_to_bytes(order, 32)
+
+ ec_p256_context = VoidPointer()
+ result = _ec_lib.ec_ws_new_context(ec_p256_context.address_of(),
+ c_uint8_ptr(p256_modulus),
+ c_uint8_ptr(p256_b),
+ c_uint8_ptr(p256_order),
+ c_size_t(len(p256_modulus)),
+ c_ulonglong(getrandbits(64))
+ )
+ if result:
+ raise ImportError("Error %d initializing P-256 context" % result)
+
+ context = SmartPointer(ec_p256_context.get(), _ec_lib.ec_ws_free_context)
+ p256 = _Curve(Integer(p),
+ Integer(b),
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 256,
+ "1.2.840.10045.3.1.7", # ANSI X9.62 / SEC2
+ context,
+ "NIST P-256",
+ "ecdsa-sha2-nistp256",
+ EcLib)
+ return p256
+
+
+def p384_curve():
+ p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff
+ b = 0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef
+ order = 0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973
+ Gx = 0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760aB7
+ Gy = 0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5F
+
+ p384_modulus = long_to_bytes(p, 48)
+ p384_b = long_to_bytes(b, 48)
+ p384_order = long_to_bytes(order, 48)
+
+ ec_p384_context = VoidPointer()
+ result = _ec_lib.ec_ws_new_context(ec_p384_context.address_of(),
+ c_uint8_ptr(p384_modulus),
+ c_uint8_ptr(p384_b),
+ c_uint8_ptr(p384_order),
+ c_size_t(len(p384_modulus)),
+ c_ulonglong(getrandbits(64))
+ )
+ if result:
+ raise ImportError("Error %d initializing P-384 context" % result)
+
+ context = SmartPointer(ec_p384_context.get(), _ec_lib.ec_ws_free_context)
+ p384 = _Curve(Integer(p),
+ Integer(b),
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 384,
+ "1.3.132.0.34", # SEC 2
+ context,
+ "NIST P-384",
+ "ecdsa-sha2-nistp384",
+ EcLib)
+ return p384
+
+
+def p521_curve():
+ p = 0x000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ b = 0x00000051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00
+ order = 0x000001fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409
+ Gx = 0x000000c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66
+ Gy = 0x0000011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650
+
+ p521_modulus = long_to_bytes(p, 66)
+ p521_b = long_to_bytes(b, 66)
+ p521_order = long_to_bytes(order, 66)
+
+ ec_p521_context = VoidPointer()
+ result = _ec_lib.ec_ws_new_context(ec_p521_context.address_of(),
+ c_uint8_ptr(p521_modulus),
+ c_uint8_ptr(p521_b),
+ c_uint8_ptr(p521_order),
+ c_size_t(len(p521_modulus)),
+ c_ulonglong(getrandbits(64))
+ )
+ if result:
+ raise ImportError("Error %d initializing P-521 context" % result)
+
+ context = SmartPointer(ec_p521_context.get(), _ec_lib.ec_ws_free_context)
+ p521 = _Curve(Integer(p),
+ Integer(b),
+ Integer(order),
+ Integer(Gx),
+ Integer(Gy),
+ None,
+ 521,
+ "1.3.132.0.35", # SEC 2
+ context,
+ "NIST P-521",
+ "ecdsa-sha2-nistp521",
+ EcLib)
+ return p521
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_openssh.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_openssh.py
new file mode 100644
index 00000000..88dacfc2
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_openssh.py
@@ -0,0 +1,135 @@
+# ===================================================================
+#
+# Copyright (c) 2019, Helder Eijs
+# 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 struct
+
+from Crypto.Cipher import AES
+from Crypto.Hash import SHA512
+from Crypto.Protocol.KDF import _bcrypt_hash
+from Crypto.Util.strxor import strxor
+from Crypto.Util.py3compat import tostr, bchr, bord
+
+
+def read_int4(data):
+ if len(data) < 4:
+ raise ValueError("Insufficient data")
+ value = struct.unpack(">I", data[:4])[0]
+ return value, data[4:]
+
+
+def read_bytes(data):
+ size, data = read_int4(data)
+ if len(data) < size:
+ raise ValueError("Insufficient data (V)")
+ return data[:size], data[size:]
+
+
+def read_string(data):
+ s, d = read_bytes(data)
+ return tostr(s), d
+
+
+def check_padding(pad):
+ for v, x in enumerate(pad):
+ if bord(x) != ((v + 1) & 0xFF):
+ raise ValueError("Incorrect padding")
+
+
+def import_openssh_private_generic(data, password):
+ # https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD
+ # https://github.com/openssh/openssh-portable/blob/master/sshkey.c
+ # https://coolaj86.com/articles/the-openssh-private-key-format/
+ # https://coolaj86.com/articles/the-ssh-public-key-format/
+
+ if not data.startswith(b'openssh-key-v1\x00'):
+ raise ValueError("Incorrect magic value")
+ data = data[15:]
+
+ ciphername, data = read_string(data)
+ kdfname, data = read_string(data)
+ kdfoptions, data = read_bytes(data)
+ number_of_keys, data = read_int4(data)
+
+ if number_of_keys != 1:
+ raise ValueError("We only handle 1 key at a time")
+
+ _, data = read_string(data) # Public key
+ encrypted, data = read_bytes(data)
+ if data:
+ raise ValueError("Too much data")
+
+ if len(encrypted) % 8 != 0:
+ raise ValueError("Incorrect payload length")
+
+ # Decrypt if necessary
+ if ciphername == 'none':
+ decrypted = encrypted
+ else:
+ if (ciphername, kdfname) != ('aes256-ctr', 'bcrypt'):
+ raise ValueError("Unsupported encryption scheme %s/%s" % (ciphername, kdfname))
+
+ salt, kdfoptions = read_bytes(kdfoptions)
+ iterations, kdfoptions = read_int4(kdfoptions)
+
+ if len(salt) != 16:
+ raise ValueError("Incorrect salt length")
+ if kdfoptions:
+ raise ValueError("Too much data in kdfoptions")
+
+ pwd_sha512 = SHA512.new(password).digest()
+ # We need 32+16 = 48 bytes, therefore 2 bcrypt outputs are sufficient
+ stripes = []
+ constant = b"OxychromaticBlowfishSwatDynamite"
+ for count in range(1, 3):
+ salt_sha512 = SHA512.new(salt + struct.pack(">I", count)).digest()
+ out_le = _bcrypt_hash(pwd_sha512, 6, salt_sha512, constant, False)
+ out = struct.pack("IIIIIIII", out_le))
+ acc = bytearray(out)
+ for _ in range(1, iterations):
+ out_le = _bcrypt_hash(pwd_sha512, 6, SHA512.new(out).digest(), constant, False)
+ out = struct.pack("IIIIIIII", out_le))
+ strxor(acc, out, output=acc)
+ stripes.append(acc[:24])
+
+ result = b"".join([bchr(a)+bchr(b) for (a, b) in zip(*stripes)])
+
+ cipher = AES.new(result[:32],
+ AES.MODE_CTR,
+ nonce=b"",
+ initial_value=result[32:32+16])
+ decrypted = cipher.decrypt(encrypted)
+
+ checkint1, decrypted = read_int4(decrypted)
+ checkint2, decrypted = read_int4(decrypted)
+ if checkint1 != checkint2:
+ raise ValueError("Incorrect checksum")
+ ssh_name, decrypted = read_string(decrypted)
+
+ return ssh_name, decrypted
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_openssh.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_openssh.pyi
new file mode 100644
index 00000000..15f3677a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_openssh.pyi
@@ -0,0 +1,7 @@
+from typing import Tuple
+
+def read_int4(data: bytes) -> Tuple[int, bytes]: ...
+def read_bytes(data: bytes) -> Tuple[bytes, bytes]: ...
+def read_string(data: bytes) -> Tuple[str, bytes]: ...
+def check_padding(pad: bytes) -> None: ...
+def import_openssh_private_generic(data: bytes, password: bytes) -> Tuple[str, bytes]: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_point.py b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_point.py
new file mode 100644
index 00000000..ecaecd26
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_point.py
@@ -0,0 +1,493 @@
+# This file is licensed under the BSD 2-Clause License.
+# See https://opensource.org/licenses/BSD-2-Clause for details.
+
+import threading
+
+from Crypto.Util.number import bytes_to_long, long_to_bytes
+from Crypto.Util._raw_api import (VoidPointer, null_pointer,
+ SmartPointer, c_size_t, c_uint8_ptr,
+ c_ulonglong)
+from Crypto.Math.Numbers import Integer
+from Crypto.Random.random import getrandbits
+
+
+class CurveID(object):
+ P192 = 1
+ P224 = 2
+ P256 = 3
+ P384 = 4
+ P521 = 5
+ ED25519 = 6
+ ED448 = 7
+ CURVE25519 = 8
+ CURVE448 = 9
+
+
+class _Curves(object):
+
+ curves = {}
+ curves_lock = threading.RLock()
+
+ p192_names = ["p192", "NIST P-192", "P-192", "prime192v1", "secp192r1",
+ "nistp192"]
+ p224_names = ["p224", "NIST P-224", "P-224", "prime224v1", "secp224r1",
+ "nistp224"]
+ p256_names = ["p256", "NIST P-256", "P-256", "prime256v1", "secp256r1",
+ "nistp256"]
+ p384_names = ["p384", "NIST P-384", "P-384", "prime384v1", "secp384r1",
+ "nistp384"]
+ p521_names = ["p521", "NIST P-521", "P-521", "prime521v1", "secp521r1",
+ "nistp521"]
+ ed25519_names = ["ed25519", "Ed25519"]
+ ed448_names = ["ed448", "Ed448"]
+ curve25519_names = ["curve25519", "Curve25519", "X25519"]
+ curve448_names = ["curve448", "Curve448", "X448"]
+
+ all_names = p192_names + p224_names + p256_names + p384_names + p521_names + \
+ ed25519_names + ed448_names + curve25519_names + curve448_names
+
+ def __contains__(self, item):
+ return item in self.all_names
+
+ def __dir__(self):
+ return self.all_names
+
+ def load(self, name):
+ if name in self.p192_names:
+ from . import _nist_ecc
+ p192 = _nist_ecc.p192_curve()
+ p192.id = CurveID.P192
+ self.curves.update(dict.fromkeys(self.p192_names, p192))
+ elif name in self.p224_names:
+ from . import _nist_ecc
+ p224 = _nist_ecc.p224_curve()
+ p224.id = CurveID.P224
+ self.curves.update(dict.fromkeys(self.p224_names, p224))
+ elif name in self.p256_names:
+ from . import _nist_ecc
+ p256 = _nist_ecc.p256_curve()
+ p256.id = CurveID.P256
+ self.curves.update(dict.fromkeys(self.p256_names, p256))
+ elif name in self.p384_names:
+ from . import _nist_ecc
+ p384 = _nist_ecc.p384_curve()
+ p384.id = CurveID.P384
+ self.curves.update(dict.fromkeys(self.p384_names, p384))
+ elif name in self.p521_names:
+ from . import _nist_ecc
+ p521 = _nist_ecc.p521_curve()
+ p521.id = CurveID.P521
+ self.curves.update(dict.fromkeys(self.p521_names, p521))
+ elif name in self.ed25519_names:
+ from . import _edwards
+ ed25519 = _edwards.ed25519_curve()
+ ed25519.id = CurveID.ED25519
+ self.curves.update(dict.fromkeys(self.ed25519_names, ed25519))
+ elif name in self.ed448_names:
+ from . import _edwards
+ ed448 = _edwards.ed448_curve()
+ ed448.id = CurveID.ED448
+ self.curves.update(dict.fromkeys(self.ed448_names, ed448))
+ elif name in self.curve25519_names:
+ from . import _montgomery
+ curve25519 = _montgomery.curve25519_curve()
+ curve25519.id = CurveID.CURVE25519
+ self.curves.update(dict.fromkeys(self.curve25519_names, curve25519))
+ elif name in self.curve448_names:
+ from . import _montgomery
+ curve448 = _montgomery.curve448_curve()
+ curve448.id = CurveID.CURVE448
+ self.curves.update(dict.fromkeys(self.curve448_names, curve448))
+ else:
+ raise ValueError("Unsupported curve '%s'" % name)
+ return self.curves[name]
+
+ def __getitem__(self, name):
+ with self.curves_lock:
+ curve = self.curves.get(name)
+ if curve is None:
+ curve = self.load(name)
+ if name in self.curve25519_names or name in self.curve448_names:
+ curve.G = EccXPoint(curve.Gx, name)
+ else:
+ curve.G = EccPoint(curve.Gx, curve.Gy, name)
+ curve.is_edwards = curve.id in (CurveID.ED25519, CurveID.ED448)
+ curve.is_montgomery = curve.id in (CurveID.CURVE25519,
+ CurveID.CURVE448)
+ curve.is_weierstrass = not (curve.is_edwards or
+ curve.is_montgomery)
+ return curve
+
+ def items(self):
+ # Load all curves
+ for name in self.all_names:
+ _ = self[name]
+ return self.curves.items()
+
+
+_curves = _Curves()
+
+
+class EccPoint(object):
+ """A class to model a point on an Elliptic Curve.
+
+ The class supports operators for:
+
+ * Adding two points: ``R = S + T``
+ * In-place addition: ``S += T``
+ * Negating a point: ``R = -T``
+ * Comparing two points: ``if S == T: ...`` or ``if S != T: ...``
+ * Multiplying a point by a scalar: ``R = S*k``
+ * In-place multiplication by a scalar: ``T *= k``
+
+ :ivar curve: The **canonical** name of the curve as defined in the `ECC table`_.
+ :vartype curve: string
+
+ :ivar x: The affine X-coordinate of the ECC point
+ :vartype x: integer
+
+ :ivar y: The affine Y-coordinate of the ECC point
+ :vartype y: integer
+
+ :ivar xy: The tuple with affine X- and Y- coordinates
+ """
+
+ def __init__(self, x, y, curve="p256"):
+
+ try:
+ self._curve = _curves[curve]
+ except KeyError:
+ raise ValueError("Unknown curve name %s" % str(curve))
+ self.curve = self._curve.canonical
+
+ if self._curve.id == CurveID.CURVE25519:
+ raise ValueError("EccPoint cannot be created for Curve25519")
+
+ modulus_bytes = self.size_in_bytes()
+
+ xb = long_to_bytes(x, modulus_bytes)
+ yb = long_to_bytes(y, modulus_bytes)
+ if len(xb) != modulus_bytes or len(yb) != modulus_bytes:
+ raise ValueError("Incorrect coordinate length")
+
+ new_point = self._curve.rawlib.new_point
+ free_func = self._curve.rawlib.free_point
+
+ self._point = VoidPointer()
+ try:
+ context = self._curve.context.get()
+ except AttributeError:
+ context = null_pointer
+ result = new_point(self._point.address_of(),
+ c_uint8_ptr(xb),
+ c_uint8_ptr(yb),
+ c_size_t(modulus_bytes),
+ context)
+
+ if result:
+ if result == 15:
+ raise ValueError("The EC point does not belong to the curve")
+ raise ValueError("Error %d while instantiating an EC point" % result)
+
+ # Ensure that object disposal of this Python object will (eventually)
+ # free the memory allocated by the raw library for the EC point
+ self._point = SmartPointer(self._point.get(), free_func)
+
+ def set(self, point):
+ clone = self._curve.rawlib.clone
+ free_func = self._curve.rawlib.free_point
+
+ self._point = VoidPointer()
+ result = clone(self._point.address_of(),
+ point._point.get())
+
+ if result:
+ raise ValueError("Error %d while cloning an EC point" % result)
+
+ self._point = SmartPointer(self._point.get(), free_func)
+ return self
+
+ def __eq__(self, point):
+ if not isinstance(point, EccPoint):
+ return False
+
+ cmp_func = self._curve.rawlib.cmp
+ return 0 == cmp_func(self._point.get(), point._point.get())
+
+ # Only needed for Python 2
+ def __ne__(self, point):
+ return not self == point
+
+ def __neg__(self):
+ neg_func = self._curve.rawlib.neg
+ np = self.copy()
+ result = neg_func(np._point.get())
+ if result:
+ raise ValueError("Error %d while inverting an EC point" % result)
+ return np
+
+ def copy(self):
+ """Return a copy of this point."""
+ x, y = self.xy
+ np = EccPoint(x, y, self.curve)
+ return np
+
+ def is_point_at_infinity(self):
+ """``True`` if this is the *point-at-infinity*."""
+
+ if self._curve.is_edwards:
+ return self.x == 0
+ else:
+ return self.xy == (0, 0)
+
+ def point_at_infinity(self):
+ """Return the *point-at-infinity* for the curve."""
+
+ if self._curve.is_edwards:
+ return EccPoint(0, 1, self.curve)
+ else:
+ return EccPoint(0, 0, self.curve)
+
+ @property
+ def x(self):
+ return self.xy[0]
+
+ @property
+ def y(self):
+ return self.xy[1]
+
+ @property
+ def xy(self):
+ modulus_bytes = self.size_in_bytes()
+ xb = bytearray(modulus_bytes)
+ yb = bytearray(modulus_bytes)
+ get_xy = self._curve.rawlib.get_xy
+ result = get_xy(c_uint8_ptr(xb),
+ c_uint8_ptr(yb),
+ c_size_t(modulus_bytes),
+ self._point.get())
+ if result:
+ raise ValueError("Error %d while encoding an EC point" % result)
+
+ return (Integer(bytes_to_long(xb)), Integer(bytes_to_long(yb)))
+
+ def size_in_bytes(self):
+ """Size of each coordinate, in bytes."""
+ return (self.size_in_bits() + 7) // 8
+
+ def size_in_bits(self):
+ """Size of each coordinate, in bits."""
+ return self._curve.modulus_bits
+
+ def double(self):
+ """Double this point (in-place operation).
+
+ Returns:
+ This same object (to enable chaining).
+ """
+
+ double_func = self._curve.rawlib.double
+ result = double_func(self._point.get())
+ if result:
+ raise ValueError("Error %d while doubling an EC point" % result)
+ return self
+
+ def __iadd__(self, point):
+ """Add a second point to this one"""
+
+ add_func = self._curve.rawlib.add
+ result = add_func(self._point.get(), point._point.get())
+ if result:
+ if result == 16:
+ raise ValueError("EC points are not on the same curve")
+ raise ValueError("Error %d while adding two EC points" % result)
+ return self
+
+ def __add__(self, point):
+ """Return a new point, the addition of this one and another"""
+
+ np = self.copy()
+ np += point
+ return np
+
+ def __imul__(self, scalar):
+ """Multiply this point by a scalar"""
+
+ scalar_func = self._curve.rawlib.scalar
+ if scalar < 0:
+ raise ValueError("Scalar multiplication is only defined for non-negative integers")
+ sb = long_to_bytes(scalar)
+ result = scalar_func(self._point.get(),
+ c_uint8_ptr(sb),
+ c_size_t(len(sb)),
+ c_ulonglong(getrandbits(64)))
+ if result:
+ raise ValueError("Error %d during scalar multiplication" % result)
+ return self
+
+ def __mul__(self, scalar):
+ """Return a new point, the scalar product of this one"""
+
+ np = self.copy()
+ np *= scalar
+ return np
+
+ def __rmul__(self, left_hand):
+ return self.__mul__(left_hand)
+
+
+class EccXPoint(object):
+ """A class to model a point on an Elliptic Curve,
+ where only the X-coordinate is exposed.
+
+ The class supports operators for:
+
+ * Multiplying a point by a scalar: ``R = S*k``
+ * In-place multiplication by a scalar: ``T *= k``
+
+ :ivar curve: The **canonical** name of the curve as defined in the `ECC table`_.
+ :vartype curve: string
+
+ :ivar x: The affine X-coordinate of the ECC point
+ :vartype x: integer
+ """
+
+ def __init__(self, x, curve):
+ # Once encoded, x must not exceed the length of the modulus,
+ # but its value may match or exceed the modulus itself
+ # (i.e., non-canonical value)
+
+ try:
+ self._curve = _curves[curve]
+ except KeyError:
+ raise ValueError("Unknown curve name %s" % str(curve))
+ self.curve = self._curve.canonical
+
+ if self._curve.id not in (CurveID.CURVE25519, CurveID.CURVE448):
+ raise ValueError("EccXPoint can only be created for Curve25519/Curve448")
+
+ new_point = self._curve.rawlib.new_point
+ free_func = self._curve.rawlib.free_point
+
+ self._point = VoidPointer()
+ try:
+ context = self._curve.context.get()
+ except AttributeError:
+ context = null_pointer
+
+ modulus_bytes = self.size_in_bytes()
+
+ if x is None:
+ xb = null_pointer
+ else:
+ xb = c_uint8_ptr(long_to_bytes(x, modulus_bytes))
+ if len(xb) != modulus_bytes:
+ raise ValueError("Incorrect coordinate length")
+
+ self._point = VoidPointer()
+ result = new_point(self._point.address_of(),
+ xb,
+ c_size_t(modulus_bytes),
+ context)
+
+ if result == 15:
+ raise ValueError("The EC point does not belong to the curve")
+ if result:
+ raise ValueError("Error %d while instantiating an EC point" % result)
+
+ # Ensure that object disposal of this Python object will (eventually)
+ # free the memory allocated by the raw library for the EC point
+ self._point = SmartPointer(self._point.get(), free_func)
+
+ def set(self, point):
+ clone = self._curve.rawlib.clone
+ free_func = self._curve.rawlib.free_point
+
+ self._point = VoidPointer()
+ result = clone(self._point.address_of(),
+ point._point.get())
+ if result:
+ raise ValueError("Error %d while cloning an EC point" % result)
+
+ self._point = SmartPointer(self._point.get(), free_func)
+ return self
+
+ def __eq__(self, point):
+ if not isinstance(point, EccXPoint):
+ return False
+
+ cmp_func = self._curve.rawlib.cmp
+ p1 = self._point.get()
+ p2 = point._point.get()
+ res = cmp_func(p1, p2)
+ return 0 == res
+
+ def copy(self):
+ """Return a copy of this point."""
+
+ try:
+ x = self.x
+ except ValueError:
+ return self.point_at_infinity()
+ return EccXPoint(x, self.curve)
+
+ def is_point_at_infinity(self):
+ """``True`` if this is the *point-at-infinity*."""
+
+ try:
+ _ = self.x
+ except ValueError:
+ return True
+ return False
+
+ def point_at_infinity(self):
+ """Return the *point-at-infinity* for the curve."""
+
+ return EccXPoint(None, self.curve)
+
+ @property
+ def x(self):
+ modulus_bytes = self.size_in_bytes()
+ xb = bytearray(modulus_bytes)
+ get_x = self._curve.rawlib.get_x
+ result = get_x(c_uint8_ptr(xb),
+ c_size_t(modulus_bytes),
+ self._point.get())
+ if result == 19: # ERR_ECC_PAI
+ raise ValueError("No X coordinate for the point at infinity")
+ if result:
+ raise ValueError("Error %d while getting X of an EC point" % result)
+ return Integer(bytes_to_long(xb))
+
+ def size_in_bytes(self):
+ """Size of each coordinate, in bytes."""
+ return (self.size_in_bits() + 7) // 8
+
+ def size_in_bits(self):
+ """Size of each coordinate, in bits."""
+ return self._curve.modulus_bits
+
+ def __imul__(self, scalar):
+ """Multiply this point by a scalar"""
+
+ scalar_func = self._curve.rawlib.scalar
+ if scalar < 0:
+ raise ValueError("Scalar multiplication is only defined for non-negative integers")
+ sb = long_to_bytes(scalar)
+ result = scalar_func(self._point.get(),
+ c_uint8_ptr(sb),
+ c_size_t(len(sb)),
+ c_ulonglong(getrandbits(64)))
+ if result:
+ raise ValueError("Error %d during scalar multiplication" % result)
+ return self
+
+ def __mul__(self, scalar):
+ """Return a new point, the scalar product of this one"""
+
+ np = self.copy()
+ np *= scalar
+ return np
+
+ def __rmul__(self, left_hand):
+ return self.__mul__(left_hand)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_point.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_point.pyi
new file mode 100644
index 00000000..83f9c3ab
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/PublicKey/_point.pyi
@@ -0,0 +1,49 @@
+from typing import Union, Optional, Tuple
+
+from Crypto.Math.Numbers import Integer
+
+class EccPoint(object):
+ curve: str
+ def __init__(self,
+ x: Union[int, Integer],
+ y: Union[int, Integer],
+ curve: Optional[str] = ...) -> None: ...
+
+ def set(self, point: EccPoint) -> EccPoint: ...
+ def __eq__(self, point: object) -> bool: ...
+ def __neg__(self) -> EccPoint: ...
+ def copy(self) -> EccPoint: ...
+ def is_point_at_infinity(self) -> bool: ...
+ def point_at_infinity(self) -> EccPoint: ...
+ @property
+ def x(self) -> int: ...
+ @property
+ def y(self) -> int: ...
+ @property
+ def xy(self) -> Tuple[int, int]: ...
+ def size_in_bytes(self) -> int: ...
+ def size_in_bits(self) -> int: ...
+ def double(self) -> EccPoint: ...
+ def __iadd__(self, point: EccPoint) -> EccPoint: ...
+ def __add__(self, point: EccPoint) -> EccPoint: ...
+ def __imul__(self, scalar: int) -> EccPoint: ...
+ def __mul__(self, scalar: int) -> EccPoint: ...
+
+
+class EccXPoint(object):
+ curve: str
+ def __init__(self,
+ x: Union[int, Integer],
+ curve: Optional[str] = ...) -> None: ...
+ def set(self, point: EccXPoint) -> EccXPoint: ...
+ def __eq__(self, point: object) -> bool: ...
+ def copy(self) -> EccXPoint: ...
+ def is_point_at_infinity(self) -> bool: ...
+ def point_at_infinity(self) -> EccXPoint: ...
+ @property
+ def x(self) -> int: ...
+ def size_in_bytes(self) -> int: ...
+ def size_in_bits(self) -> int: ...
+ def __imul__(self, scalar: int) -> EccXPoint: ...
+ def __mul__(self, scalar: int) -> EccXPoint: ...
+ def __rmul__(self, left_hand: int) -> EccXPoint: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Random/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/Random/__init__.py
new file mode 100644
index 00000000..0f83a07e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Random/__init__.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+#
+# Random/__init__.py : PyCrypto random number generation
+#
+# ===================================================================
+# 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', 'get_random_bytes']
+
+from os import urandom
+
+class _UrandomRNG(object):
+
+ def read(self, n):
+ """Return a random byte string of the desired size."""
+ return urandom(n)
+
+ def flush(self):
+ """Method provided for backward compatibility only."""
+ pass
+
+ def reinit(self):
+ """Method provided for backward compatibility only."""
+ pass
+
+ def close(self):
+ """Method provided for backward compatibility only."""
+ pass
+
+
+def new(*args, **kwargs):
+ """Return a file-like object that outputs cryptographically random bytes."""
+ return _UrandomRNG()
+
+
+def atfork():
+ pass
+
+
+#: Function that returns a random byte string of the desired size.
+get_random_bytes = urandom
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Random/__init__.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Random/__init__.pyi
new file mode 100644
index 00000000..ddc5b9b0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Random/__init__.pyi
@@ -0,0 +1,19 @@
+from typing import Any
+
+__all__ = ['new', 'get_random_bytes']
+
+from os import urandom
+
+class _UrandomRNG(object):
+
+ def read(self, n: int) -> bytes:...
+ def flush(self) -> None: ...
+ def reinit(self) -> None: ...
+ def close(self) -> None: ...
+
+def new(*args: Any, **kwargs: Any) -> _UrandomRNG: ...
+
+def atfork() -> None: ...
+
+get_random_bytes = urandom
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Random/random.py b/backend/venv/lib/python3.9/site-packages/Crypto/Random/random.py
new file mode 100644
index 00000000..5389b3bb
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Random/random.py
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+#
+# Random/random.py : Strong alternative for the standard 'random' module
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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__ = ['StrongRandom', 'getrandbits', 'randrange', 'randint', 'choice', 'shuffle', 'sample']
+
+from Crypto import Random
+
+from Crypto.Util.py3compat import is_native_int
+
+class StrongRandom(object):
+ def __init__(self, rng=None, randfunc=None):
+ if randfunc is None and rng is None:
+ self._randfunc = None
+ elif randfunc is not None and rng is None:
+ self._randfunc = randfunc
+ elif randfunc is None and rng is not None:
+ self._randfunc = rng.read
+ else:
+ raise ValueError("Cannot specify both 'rng' and 'randfunc'")
+
+ def getrandbits(self, k):
+ """Return an integer with k random bits."""
+
+ if self._randfunc is None:
+ self._randfunc = Random.new().read
+ mask = (1 << k) - 1
+ return mask & bytes_to_long(self._randfunc(ceil_div(k, 8)))
+
+ def randrange(self, *args):
+ """randrange([start,] stop[, step]):
+ Return a randomly-selected element from range(start, stop, step)."""
+ if len(args) == 3:
+ (start, stop, step) = args
+ elif len(args) == 2:
+ (start, stop) = args
+ step = 1
+ elif len(args) == 1:
+ (stop,) = args
+ start = 0
+ step = 1
+ else:
+ raise TypeError("randrange expected at most 3 arguments, got %d" % (len(args),))
+ if (not is_native_int(start) or not is_native_int(stop) or not
+ is_native_int(step)):
+ raise TypeError("randrange requires integer arguments")
+ if step == 0:
+ raise ValueError("randrange step argument must not be zero")
+
+ num_choices = ceil_div(stop - start, step)
+ if num_choices < 0:
+ num_choices = 0
+ if num_choices < 1:
+ raise ValueError("empty range for randrange(%r, %r, %r)" % (start, stop, step))
+
+ # Pick a random number in the range of possible numbers
+ r = num_choices
+ while r >= num_choices:
+ r = self.getrandbits(size(num_choices))
+
+ return start + (step * r)
+
+ def randint(self, a, b):
+ """Return a random integer N such that a <= N <= b."""
+ if not is_native_int(a) or not is_native_int(b):
+ raise TypeError("randint requires integer arguments")
+ N = self.randrange(a, b+1)
+ assert a <= N <= b
+ return N
+
+ def choice(self, seq):
+ """Return a random element from a (non-empty) sequence.
+
+ If the seqence is empty, raises IndexError.
+ """
+ if len(seq) == 0:
+ raise IndexError("empty sequence")
+ return seq[self.randrange(len(seq))]
+
+ def shuffle(self, x):
+ """Shuffle the sequence in place."""
+ # Fisher-Yates shuffle. O(n)
+ # See http://en.wikipedia.org/wiki/Fisher-Yates_shuffle
+ # Working backwards from the end of the array, we choose a random item
+ # from the remaining items until all items have been chosen.
+ for i in range(len(x)-1, 0, -1): # iterate from len(x)-1 downto 1
+ j = self.randrange(0, i+1) # choose random j such that 0 <= j <= i
+ x[i], x[j] = x[j], x[i] # exchange x[i] and x[j]
+
+ def sample(self, population, k):
+ """Return a k-length list of unique elements chosen from the population sequence."""
+
+ num_choices = len(population)
+ if k > num_choices:
+ raise ValueError("sample larger than population")
+
+ retval = []
+ selected = {} # we emulate a set using a dict here
+ for i in range(k):
+ r = None
+ while r is None or r in selected:
+ r = self.randrange(num_choices)
+ retval.append(population[r])
+ selected[r] = 1
+ return retval
+
+_r = StrongRandom()
+getrandbits = _r.getrandbits
+randrange = _r.randrange
+randint = _r.randint
+choice = _r.choice
+shuffle = _r.shuffle
+sample = _r.sample
+
+# These are at the bottom to avoid problems with recursive imports
+from Crypto.Util.number import ceil_div, bytes_to_long, long_to_bytes, size
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Random/random.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Random/random.pyi
new file mode 100644
index 00000000..9b7cf7ee
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Random/random.pyi
@@ -0,0 +1,22 @@
+from typing import Callable, Tuple, Union, Sequence, Any, Optional, TypeVar
+
+__all__ = ['StrongRandom', 'getrandbits', 'randrange', 'randint', 'choice', 'shuffle', 'sample']
+
+T = TypeVar('T')
+
+class StrongRandom(object):
+ def __init__(self, rng: Optional[Any]=None, randfunc: Optional[Callable]=None) -> None: ... # TODO What is rng?
+ def getrandbits(self, k: int) -> int: ...
+ def randrange(self, start: int, stop: int = ..., step: int = ...) -> int: ...
+ def randint(self, a: int, b: int) -> int: ...
+ def choice(self, seq: Sequence[T]) -> T: ...
+ def shuffle(self, x: Sequence) -> None: ...
+ def sample(self, population: Sequence, k: int) -> list: ...
+
+_r = StrongRandom()
+getrandbits = _r.getrandbits
+randrange = _r.randrange
+randint = _r.randint
+choice = _r.choice
+shuffle = _r.shuffle
+sample = _r.sample
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/__init__.py
new file mode 100644
index 00000000..5cc75dba
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/__init__.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/__init__.py: Self-test for cipher modules
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test for cipher modules"""
+
+import sys
+
+
+def get_tests(config={}):
+ tests = []
+ from Crypto.SelfTest.Cipher import test_AES; tests += test_AES.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_ARC2; tests += test_ARC2.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_ARC4; tests += test_ARC4.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_Blowfish; tests += test_Blowfish.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_CAST; tests += test_CAST.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_DES3; tests += test_DES3.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_DES; tests += test_DES.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_Salsa20; tests += test_Salsa20.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_ChaCha20; tests += test_ChaCha20.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_ChaCha20_Poly1305; tests += test_ChaCha20_Poly1305.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_pkcs1_15; tests += test_pkcs1_15.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_pkcs1_oaep; tests += test_pkcs1_oaep.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_OCB; tests += test_OCB.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_CBC; tests += test_CBC.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_CFB; tests += test_CFB.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_OpenPGP; tests += test_OpenPGP.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_OFB; tests += test_OFB.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_CTR; tests += test_CTR.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_CCM; tests += test_CCM.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_EAX; tests += test_EAX.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_GCM; tests += test_GCM.get_tests(config=config)
+ from Crypto.SelfTest.Cipher import test_SIV; tests += test_SIV.get_tests(config=config)
+
+ if sys.version_info >= (3, 9):
+ from Crypto.SelfTest.Cipher import test_KW
+ tests += test_KW.get_tests(config=config)
+
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/common.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/common.py
new file mode 100644
index 00000000..c5bc755a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/common.py
@@ -0,0 +1,510 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/common.py: Common code for Crypto.SelfTest.Hash
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-testing for PyCrypto hash modules"""
+
+import unittest
+from binascii import a2b_hex, b2a_hex, hexlify
+
+from Crypto.Util.py3compat import b
+from Crypto.Util.strxor import strxor_c
+
+class _NoDefault: pass # sentinel object
+def _extract(d, k, default=_NoDefault):
+ """Get an item from a dictionary, and remove it from the dictionary."""
+ try:
+ retval = d[k]
+ except KeyError:
+ if default is _NoDefault:
+ raise
+ return default
+ del d[k]
+ return retval
+
+# Generic cipher test case
+class CipherSelfTest(unittest.TestCase):
+
+ def __init__(self, module, params):
+ unittest.TestCase.__init__(self)
+ self.module = module
+
+ # Extract the parameters
+ params = params.copy()
+ self.description = _extract(params, 'description')
+ self.key = b(_extract(params, 'key'))
+ self.plaintext = b(_extract(params, 'plaintext'))
+ self.ciphertext = b(_extract(params, 'ciphertext'))
+ self.module_name = _extract(params, 'module_name', None)
+ self.assoc_data = _extract(params, 'assoc_data', None)
+ self.mac = _extract(params, 'mac', None)
+ if self.assoc_data:
+ self.mac = b(self.mac)
+
+ mode = _extract(params, 'mode', None)
+ self.mode_name = str(mode)
+
+ if mode is not None:
+ # Block cipher
+ self.mode = getattr(self.module, "MODE_" + mode)
+
+ self.iv = _extract(params, 'iv', None)
+ if self.iv is None:
+ self.iv = _extract(params, 'nonce', None)
+ if self.iv is not None:
+ self.iv = b(self.iv)
+
+ else:
+ # Stream cipher
+ self.mode = None
+ self.iv = _extract(params, 'iv', None)
+ if self.iv is not None:
+ self.iv = b(self.iv)
+
+ self.extra_params = params
+
+ def shortDescription(self):
+ return self.description
+
+ def _new(self):
+ params = self.extra_params.copy()
+ key = a2b_hex(self.key)
+
+ old_style = []
+ if self.mode is not None:
+ old_style = [ self.mode ]
+ if self.iv is not None:
+ old_style += [ a2b_hex(self.iv) ]
+
+ return self.module.new(key, *old_style, **params)
+
+ def isMode(self, name):
+ if not hasattr(self.module, "MODE_"+name):
+ return False
+ return self.mode == getattr(self.module, "MODE_"+name)
+
+ def runTest(self):
+ plaintext = a2b_hex(self.plaintext)
+ ciphertext = a2b_hex(self.ciphertext)
+ assoc_data = []
+ if self.assoc_data:
+ assoc_data = [ a2b_hex(b(x)) for x in self.assoc_data]
+
+ ct = None
+ pt = None
+
+ #
+ # Repeat the same encryption or decryption twice and verify
+ # that the result is always the same
+ #
+ for i in range(2):
+ cipher = self._new()
+ decipher = self._new()
+
+ # Only AEAD modes
+ for comp in assoc_data:
+ cipher.update(comp)
+ decipher.update(comp)
+
+ ctX = b2a_hex(cipher.encrypt(plaintext))
+ ptX = b2a_hex(decipher.decrypt(ciphertext))
+
+ if ct:
+ self.assertEqual(ct, ctX)
+ self.assertEqual(pt, ptX)
+ ct, pt = ctX, ptX
+
+ self.assertEqual(self.ciphertext, ct) # encrypt
+ self.assertEqual(self.plaintext, pt) # decrypt
+
+ if self.mac:
+ mac = b2a_hex(cipher.digest())
+ self.assertEqual(self.mac, mac)
+ decipher.verify(a2b_hex(self.mac))
+
+class CipherStreamingSelfTest(CipherSelfTest):
+
+ def shortDescription(self):
+ desc = self.module_name
+ if self.mode is not None:
+ desc += " in %s mode" % (self.mode_name,)
+ return "%s should behave like a stream cipher" % (desc,)
+
+ def runTest(self):
+ plaintext = a2b_hex(self.plaintext)
+ ciphertext = a2b_hex(self.ciphertext)
+
+ # The cipher should work like a stream cipher
+
+ # Test counter mode encryption, 3 bytes at a time
+ ct3 = []
+ cipher = self._new()
+ for i in range(0, len(plaintext), 3):
+ ct3.append(cipher.encrypt(plaintext[i:i+3]))
+ ct3 = b2a_hex(b("").join(ct3))
+ self.assertEqual(self.ciphertext, ct3) # encryption (3 bytes at a time)
+
+ # Test counter mode decryption, 3 bytes at a time
+ pt3 = []
+ cipher = self._new()
+ for i in range(0, len(ciphertext), 3):
+ pt3.append(cipher.encrypt(ciphertext[i:i+3]))
+ # PY3K: This is meant to be text, do not change to bytes (data)
+ pt3 = b2a_hex(b("").join(pt3))
+ self.assertEqual(self.plaintext, pt3) # decryption (3 bytes at a time)
+
+
+class RoundtripTest(unittest.TestCase):
+ def __init__(self, module, params):
+ from Crypto import Random
+ unittest.TestCase.__init__(self)
+ self.module = module
+ self.iv = Random.get_random_bytes(module.block_size)
+ self.key = b(params['key'])
+ self.plaintext = 100 * b(params['plaintext'])
+ self.module_name = params.get('module_name', None)
+
+ def shortDescription(self):
+ return """%s .decrypt() output of .encrypt() should not be garbled""" % (self.module_name,)
+
+ def runTest(self):
+
+ ## ECB mode
+ mode = self.module.MODE_ECB
+ encryption_cipher = self.module.new(a2b_hex(self.key), mode)
+ ciphertext = encryption_cipher.encrypt(self.plaintext)
+ decryption_cipher = self.module.new(a2b_hex(self.key), mode)
+ decrypted_plaintext = decryption_cipher.decrypt(ciphertext)
+ self.assertEqual(self.plaintext, decrypted_plaintext)
+
+
+class IVLengthTest(unittest.TestCase):
+ def __init__(self, module, params):
+ unittest.TestCase.__init__(self)
+ self.module = module
+ self.key = b(params['key'])
+
+ def shortDescription(self):
+ return "Check that all modes except MODE_ECB and MODE_CTR require an IV of the proper length"
+
+ def runTest(self):
+ self.assertRaises(TypeError, self.module.new, a2b_hex(self.key),
+ self.module.MODE_ECB, b(""))
+
+ def _dummy_counter(self):
+ return "\0" * self.module.block_size
+
+
+class NoDefaultECBTest(unittest.TestCase):
+ def __init__(self, module, params):
+ unittest.TestCase.__init__(self)
+ self.module = module
+ self.key = b(params['key'])
+
+ def runTest(self):
+ self.assertRaises(TypeError, self.module.new, a2b_hex(self.key))
+
+
+class BlockSizeTest(unittest.TestCase):
+ def __init__(self, module, params):
+ unittest.TestCase.__init__(self)
+ self.module = module
+ self.key = a2b_hex(b(params['key']))
+
+ def runTest(self):
+ cipher = self.module.new(self.key, self.module.MODE_ECB)
+ self.assertEqual(cipher.block_size, self.module.block_size)
+
+
+class ByteArrayTest(unittest.TestCase):
+ """Verify we can use bytearray's for encrypting and decrypting"""
+
+ def __init__(self, module, params):
+ unittest.TestCase.__init__(self)
+ self.module = module
+
+ # Extract the parameters
+ params = params.copy()
+ self.description = _extract(params, 'description')
+ self.key = b(_extract(params, 'key'))
+ self.plaintext = b(_extract(params, 'plaintext'))
+ self.ciphertext = b(_extract(params, 'ciphertext'))
+ self.module_name = _extract(params, 'module_name', None)
+ self.assoc_data = _extract(params, 'assoc_data', None)
+ self.mac = _extract(params, 'mac', None)
+ if self.assoc_data:
+ self.mac = b(self.mac)
+
+ mode = _extract(params, 'mode', None)
+ self.mode_name = str(mode)
+
+ if mode is not None:
+ # Block cipher
+ self.mode = getattr(self.module, "MODE_" + mode)
+
+ self.iv = _extract(params, 'iv', None)
+ if self.iv is None:
+ self.iv = _extract(params, 'nonce', None)
+ if self.iv is not None:
+ self.iv = b(self.iv)
+ else:
+ # Stream cipher
+ self.mode = None
+ self.iv = _extract(params, 'iv', None)
+ if self.iv is not None:
+ self.iv = b(self.iv)
+
+ self.extra_params = params
+
+ def _new(self):
+ params = self.extra_params.copy()
+ key = a2b_hex(self.key)
+
+ old_style = []
+ if self.mode is not None:
+ old_style = [ self.mode ]
+ if self.iv is not None:
+ old_style += [ a2b_hex(self.iv) ]
+
+ return self.module.new(key, *old_style, **params)
+
+ def runTest(self):
+
+ plaintext = a2b_hex(self.plaintext)
+ ciphertext = a2b_hex(self.ciphertext)
+ assoc_data = []
+ if self.assoc_data:
+ assoc_data = [ bytearray(a2b_hex(b(x))) for x in self.assoc_data]
+
+ cipher = self._new()
+ decipher = self._new()
+
+ # Only AEAD modes
+ for comp in assoc_data:
+ cipher.update(comp)
+ decipher.update(comp)
+
+ ct = b2a_hex(cipher.encrypt(bytearray(plaintext)))
+ pt = b2a_hex(decipher.decrypt(bytearray(ciphertext)))
+
+ self.assertEqual(self.ciphertext, ct) # encrypt
+ self.assertEqual(self.plaintext, pt) # decrypt
+
+ if self.mac:
+ mac = b2a_hex(cipher.digest())
+ self.assertEqual(self.mac, mac)
+ decipher.verify(bytearray(a2b_hex(self.mac)))
+
+
+class MemoryviewTest(unittest.TestCase):
+ """Verify we can use memoryviews for encrypting and decrypting"""
+
+ def __init__(self, module, params):
+ unittest.TestCase.__init__(self)
+ self.module = module
+
+ # Extract the parameters
+ params = params.copy()
+ self.description = _extract(params, 'description')
+ self.key = b(_extract(params, 'key'))
+ self.plaintext = b(_extract(params, 'plaintext'))
+ self.ciphertext = b(_extract(params, 'ciphertext'))
+ self.module_name = _extract(params, 'module_name', None)
+ self.assoc_data = _extract(params, 'assoc_data', None)
+ self.mac = _extract(params, 'mac', None)
+ if self.assoc_data:
+ self.mac = b(self.mac)
+
+ mode = _extract(params, 'mode', None)
+ self.mode_name = str(mode)
+
+ if mode is not None:
+ # Block cipher
+ self.mode = getattr(self.module, "MODE_" + mode)
+
+ self.iv = _extract(params, 'iv', None)
+ if self.iv is None:
+ self.iv = _extract(params, 'nonce', None)
+ if self.iv is not None:
+ self.iv = b(self.iv)
+ else:
+ # Stream cipher
+ self.mode = None
+ self.iv = _extract(params, 'iv', None)
+ if self.iv is not None:
+ self.iv = b(self.iv)
+
+ self.extra_params = params
+
+ def _new(self):
+ params = self.extra_params.copy()
+ key = a2b_hex(self.key)
+
+ old_style = []
+ if self.mode is not None:
+ old_style = [ self.mode ]
+ if self.iv is not None:
+ old_style += [ a2b_hex(self.iv) ]
+
+ return self.module.new(key, *old_style, **params)
+
+ def runTest(self):
+
+ plaintext = a2b_hex(self.plaintext)
+ ciphertext = a2b_hex(self.ciphertext)
+ assoc_data = []
+ if self.assoc_data:
+ assoc_data = [ memoryview(a2b_hex(b(x))) for x in self.assoc_data]
+
+ cipher = self._new()
+ decipher = self._new()
+
+ # Only AEAD modes
+ for comp in assoc_data:
+ cipher.update(comp)
+ decipher.update(comp)
+
+ ct = b2a_hex(cipher.encrypt(memoryview(plaintext)))
+ pt = b2a_hex(decipher.decrypt(memoryview(ciphertext)))
+
+ self.assertEqual(self.ciphertext, ct) # encrypt
+ self.assertEqual(self.plaintext, pt) # decrypt
+
+ if self.mac:
+ mac = b2a_hex(cipher.digest())
+ self.assertEqual(self.mac, mac)
+ decipher.verify(memoryview(a2b_hex(self.mac)))
+
+
+def make_block_tests(module, module_name, test_data, additional_params=dict()):
+ tests = []
+ extra_tests_added = False
+ for i in range(len(test_data)):
+ row = test_data[i]
+
+ # Build the "params" dictionary with
+ # - plaintext
+ # - ciphertext
+ # - key
+ # - mode (default is ECB)
+ # - (optionally) description
+ # - (optionally) any other parameter that this cipher mode requires
+ params = {}
+ if len(row) == 3:
+ (params['plaintext'], params['ciphertext'], params['key']) = row
+ elif len(row) == 4:
+ (params['plaintext'], params['ciphertext'], params['key'], params['description']) = row
+ elif len(row) == 5:
+ (params['plaintext'], params['ciphertext'], params['key'], params['description'], extra_params) = row
+ params.update(extra_params)
+ else:
+ raise AssertionError("Unsupported tuple size %d" % (len(row),))
+
+ if not "mode" in params:
+ params["mode"] = "ECB"
+
+ # Build the display-name for the test
+ p2 = params.copy()
+ p_key = _extract(p2, 'key')
+ p_plaintext = _extract(p2, 'plaintext')
+ p_ciphertext = _extract(p2, 'ciphertext')
+ p_mode = _extract(p2, 'mode')
+ p_description = _extract(p2, 'description', None)
+
+ if p_description is not None:
+ description = p_description
+ elif p_mode == 'ECB' and not p2:
+ description = "p=%s, k=%s" % (p_plaintext, p_key)
+ else:
+ description = "p=%s, k=%s, %r" % (p_plaintext, p_key, p2)
+ name = "%s #%d: %s" % (module_name, i+1, description)
+ params['description'] = name
+ params['module_name'] = module_name
+ params.update(additional_params)
+
+ # Add extra test(s) to the test suite before the current test
+ if not extra_tests_added:
+ tests += [
+ RoundtripTest(module, params),
+ IVLengthTest(module, params),
+ NoDefaultECBTest(module, params),
+ ByteArrayTest(module, params),
+ BlockSizeTest(module, params),
+ ]
+ extra_tests_added = True
+
+ # Add the current test to the test suite
+ tests.append(CipherSelfTest(module, params))
+
+ return tests
+
+def make_stream_tests(module, module_name, test_data):
+ tests = []
+ extra_tests_added = False
+ for i in range(len(test_data)):
+ row = test_data[i]
+
+ # Build the "params" dictionary
+ params = {}
+ if len(row) == 3:
+ (params['plaintext'], params['ciphertext'], params['key']) = row
+ elif len(row) == 4:
+ (params['plaintext'], params['ciphertext'], params['key'], params['description']) = row
+ elif len(row) == 5:
+ (params['plaintext'], params['ciphertext'], params['key'], params['description'], extra_params) = row
+ params.update(extra_params)
+ else:
+ raise AssertionError("Unsupported tuple size %d" % (len(row),))
+
+ # Build the display-name for the test
+ p2 = params.copy()
+ p_key = _extract(p2, 'key')
+ p_plaintext = _extract(p2, 'plaintext')
+ p_ciphertext = _extract(p2, 'ciphertext')
+ p_description = _extract(p2, 'description', None)
+
+ if p_description is not None:
+ description = p_description
+ elif not p2:
+ description = "p=%s, k=%s" % (p_plaintext, p_key)
+ else:
+ description = "p=%s, k=%s, %r" % (p_plaintext, p_key, p2)
+ name = "%s #%d: %s" % (module_name, i+1, description)
+ params['description'] = name
+ params['module_name'] = module_name
+
+ # Add extra test(s) to the test suite before the current test
+ if not extra_tests_added:
+ tests += [
+ ByteArrayTest(module, params),
+ ]
+
+ tests.append(MemoryviewTest(module, params))
+ extra_tests_added = True
+
+ # Add the test to the test suite
+ tests.append(CipherSelfTest(module, params))
+ tests.append(CipherStreamingSelfTest(module, params))
+ return tests
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_AES.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_AES.py
new file mode 100644
index 00000000..116deec6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_AES.py
@@ -0,0 +1,1351 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/AES.py: Self-test for the AES cipher
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Cipher.AES"""
+
+from __future__ import print_function
+
+import unittest
+from Crypto.Hash import SHA256
+from Crypto.Cipher import AES
+from Crypto.Util.py3compat import *
+from binascii import hexlify
+
+# This is a list of (plaintext, ciphertext, key[, description[, params]]) tuples.
+test_data = [
+ # FIPS PUB 197 test vectors
+ # http://csrc.nist.gov/publications/fips/fips197/fips-197.pdf
+
+ ('00112233445566778899aabbccddeeff', '69c4e0d86a7b0430d8cdb78070b4c55a',
+ '000102030405060708090a0b0c0d0e0f', 'FIPS 197 C.1 (AES-128)'),
+
+ ('00112233445566778899aabbccddeeff', 'dda97ca4864cdfe06eaf70a0ec0d7191',
+ '000102030405060708090a0b0c0d0e0f1011121314151617',
+ 'FIPS 197 C.2 (AES-192)'),
+
+ ('00112233445566778899aabbccddeeff', '8ea2b7ca516745bfeafc49904b496089',
+ '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
+ 'FIPS 197 C.3 (AES-256)'),
+
+ # Rijndael128 test vectors
+ # Downloaded 2008-09-13 from
+ # http://www.iaik.tugraz.at/Research/krypto/AES/old/~rijmen/rijndael/testvalues.tar.gz
+
+ # ecb_tbl.txt, KEYSIZE=128
+ ('506812a45f08c889b97f5980038b8359', 'd8f532538289ef7d06b506a4fd5be9c9',
+ '00010203050607080a0b0c0d0f101112',
+ 'ecb-tbl-128: I=1'),
+ ('5c6d71ca30de8b8b00549984d2ec7d4b', '59ab30f4d4ee6e4ff9907ef65b1fb68c',
+ '14151617191a1b1c1e1f202123242526',
+ 'ecb-tbl-128: I=2'),
+ ('53f3f4c64f8616e4e7c56199f48f21f6', 'bf1ed2fcb2af3fd41443b56d85025cb1',
+ '28292a2b2d2e2f30323334353738393a',
+ 'ecb-tbl-128: I=3'),
+ ('a1eb65a3487165fb0f1c27ff9959f703', '7316632d5c32233edcb0780560eae8b2',
+ '3c3d3e3f41424344464748494b4c4d4e',
+ 'ecb-tbl-128: I=4'),
+ ('3553ecf0b1739558b08e350a98a39bfa', '408c073e3e2538072b72625e68b8364b',
+ '50515253555657585a5b5c5d5f606162',
+ 'ecb-tbl-128: I=5'),
+ ('67429969490b9711ae2b01dc497afde8', 'e1f94dfa776597beaca262f2f6366fea',
+ '64656667696a6b6c6e6f707173747576',
+ 'ecb-tbl-128: I=6'),
+ ('93385c1f2aec8bed192f5a8e161dd508', 'f29e986c6a1c27d7b29ffd7ee92b75f1',
+ '78797a7b7d7e7f80828384858788898a',
+ 'ecb-tbl-128: I=7'),
+ ('b5bf946be19beb8db3983b5f4c6e8ddb', '131c886a57f8c2e713aba6955e2b55b5',
+ '8c8d8e8f91929394969798999b9c9d9e',
+ 'ecb-tbl-128: I=8'),
+ ('41321ee10e21bd907227c4450ff42324', 'd2ab7662df9b8c740210e5eeb61c199d',
+ 'a0a1a2a3a5a6a7a8aaabacadafb0b1b2',
+ 'ecb-tbl-128: I=9'),
+ ('00a82f59c91c8486d12c0a80124f6089', '14c10554b2859c484cab5869bbe7c470',
+ 'b4b5b6b7b9babbbcbebfc0c1c3c4c5c6',
+ 'ecb-tbl-128: I=10'),
+ ('7ce0fd076754691b4bbd9faf8a1372fe', 'db4d498f0a49cf55445d502c1f9ab3b5',
+ 'c8c9cacbcdcecfd0d2d3d4d5d7d8d9da',
+ 'ecb-tbl-128: I=11'),
+ ('23605a8243d07764541bc5ad355b3129', '6d96fef7d66590a77a77bb2056667f7f',
+ 'dcdddedfe1e2e3e4e6e7e8e9ebecedee',
+ 'ecb-tbl-128: I=12'),
+ ('12a8cfa23ea764fd876232b4e842bc44', '316fb68edba736c53e78477bf913725c',
+ 'f0f1f2f3f5f6f7f8fafbfcfdfe010002',
+ 'ecb-tbl-128: I=13'),
+ ('bcaf32415e8308b3723e5fdd853ccc80', '6936f2b93af8397fd3a771fc011c8c37',
+ '04050607090a0b0c0e0f101113141516',
+ 'ecb-tbl-128: I=14'),
+ ('89afae685d801ad747ace91fc49adde0', 'f3f92f7a9c59179c1fcc2c2ba0b082cd',
+ '2c2d2e2f31323334363738393b3c3d3e',
+ 'ecb-tbl-128: I=15'),
+ ('f521d07b484357c4a69e76124a634216', '6a95ea659ee3889158e7a9152ff04ebc',
+ '40414243454647484a4b4c4d4f505152',
+ 'ecb-tbl-128: I=16'),
+ ('3e23b3bc065bcc152407e23896d77783', '1959338344e945670678a5d432c90b93',
+ '54555657595a5b5c5e5f606163646566',
+ 'ecb-tbl-128: I=17'),
+ ('79f0fba002be1744670e7e99290d8f52', 'e49bddd2369b83ee66e6c75a1161b394',
+ '68696a6b6d6e6f70727374757778797a',
+ 'ecb-tbl-128: I=18'),
+ ('da23fe9d5bd63e1d72e3dafbe21a6c2a', 'd3388f19057ff704b70784164a74867d',
+ '7c7d7e7f81828384868788898b8c8d8e',
+ 'ecb-tbl-128: I=19'),
+ ('e3f5698ba90b6a022efd7db2c7e6c823', '23aa03e2d5e4cd24f3217e596480d1e1',
+ 'a4a5a6a7a9aaabacaeafb0b1b3b4b5b6',
+ 'ecb-tbl-128: I=20'),
+ ('bdc2691d4f1b73d2700679c3bcbf9c6e', 'c84113d68b666ab2a50a8bdb222e91b9',
+ 'e0e1e2e3e5e6e7e8eaebecedeff0f1f2',
+ 'ecb-tbl-128: I=21'),
+ ('ba74e02093217ee1ba1b42bd5624349a', 'ac02403981cd4340b507963db65cb7b6',
+ '08090a0b0d0e0f10121314151718191a',
+ 'ecb-tbl-128: I=22'),
+ ('b5c593b5851c57fbf8b3f57715e8f680', '8d1299236223359474011f6bf5088414',
+ '6c6d6e6f71727374767778797b7c7d7e',
+ 'ecb-tbl-128: I=23'),
+ ('3da9bd9cec072381788f9387c3bbf4ee', '5a1d6ab8605505f7977e55b9a54d9b90',
+ '80818283858687888a8b8c8d8f909192',
+ 'ecb-tbl-128: I=24'),
+ ('4197f3051121702ab65d316b3c637374', '72e9c2d519cf555e4208805aabe3b258',
+ '94959697999a9b9c9e9fa0a1a3a4a5a6',
+ 'ecb-tbl-128: I=25'),
+ ('9f46c62ec4f6ee3f6e8c62554bc48ab7', 'a8f3e81c4a23a39ef4d745dffe026e80',
+ 'a8a9aaabadaeafb0b2b3b4b5b7b8b9ba',
+ 'ecb-tbl-128: I=26'),
+ ('0220673fe9e699a4ebc8e0dbeb6979c8', '546f646449d31458f9eb4ef5483aee6c',
+ 'bcbdbebfc1c2c3c4c6c7c8c9cbcccdce',
+ 'ecb-tbl-128: I=27'),
+ ('b2b99171337ded9bc8c2c23ff6f18867', '4dbe4bc84ac797c0ee4efb7f1a07401c',
+ 'd0d1d2d3d5d6d7d8dadbdcdddfe0e1e2',
+ 'ecb-tbl-128: I=28'),
+ ('a7facf4e301e984e5efeefd645b23505', '25e10bfb411bbd4d625ac8795c8ca3b3',
+ 'e4e5e6e7e9eaebeceeeff0f1f3f4f5f6',
+ 'ecb-tbl-128: I=29'),
+ ('f7c762e4a9819160fd7acfb6c4eedcdd', '315637405054ec803614e43def177579',
+ 'f8f9fafbfdfefe00020304050708090a',
+ 'ecb-tbl-128: I=30'),
+ ('9b64fc21ea08709f4915436faa70f1be', '60c5bc8a1410247295c6386c59e572a8',
+ '0c0d0e0f11121314161718191b1c1d1e',
+ 'ecb-tbl-128: I=31'),
+ ('52af2c3de07ee6777f55a4abfc100b3f', '01366fc8ca52dfe055d6a00a76471ba6',
+ '20212223252627282a2b2c2d2f303132',
+ 'ecb-tbl-128: I=32'),
+ ('2fca001224386c57aa3f968cbe2c816f', 'ecc46595516ec612449c3f581e7d42ff',
+ '34353637393a3b3c3e3f404143444546',
+ 'ecb-tbl-128: I=33'),
+ ('4149c73658a4a9c564342755ee2c132f', '6b7ffe4c602a154b06ee9c7dab5331c9',
+ '48494a4b4d4e4f50525354555758595a',
+ 'ecb-tbl-128: I=34'),
+ ('af60005a00a1772f7c07a48a923c23d2', '7da234c14039a240dd02dd0fbf84eb67',
+ '5c5d5e5f61626364666768696b6c6d6e',
+ 'ecb-tbl-128: I=35'),
+ ('6fccbc28363759914b6f0280afaf20c6', 'c7dc217d9e3604ffe7e91f080ecd5a3a',
+ '70717273757677787a7b7c7d7f808182',
+ 'ecb-tbl-128: I=36'),
+ ('7d82a43ddf4fefa2fc5947499884d386', '37785901863f5c81260ea41e7580cda5',
+ '84858687898a8b8c8e8f909193949596',
+ 'ecb-tbl-128: I=37'),
+ ('5d5a990eaab9093afe4ce254dfa49ef9', 'a07b9338e92ed105e6ad720fccce9fe4',
+ '98999a9b9d9e9fa0a2a3a4a5a7a8a9aa',
+ 'ecb-tbl-128: I=38'),
+ ('4cd1e2fd3f4434b553aae453f0ed1a02', 'ae0fb9722418cc21a7da816bbc61322c',
+ 'acadaeafb1b2b3b4b6b7b8b9bbbcbdbe',
+ 'ecb-tbl-128: I=39'),
+ ('5a2c9a9641d4299125fa1b9363104b5e', 'c826a193080ff91ffb21f71d3373c877',
+ 'c0c1c2c3c5c6c7c8cacbcccdcfd0d1d2',
+ 'ecb-tbl-128: I=40'),
+ ('b517fe34c0fa217d341740bfd4fe8dd4', '1181b11b0e494e8d8b0aa6b1d5ac2c48',
+ 'd4d5d6d7d9dadbdcdedfe0e1e3e4e5e6',
+ 'ecb-tbl-128: I=41'),
+ ('014baf2278a69d331d5180103643e99a', '6743c3d1519ab4f2cd9a78ab09a511bd',
+ 'e8e9eaebedeeeff0f2f3f4f5f7f8f9fa',
+ 'ecb-tbl-128: I=42'),
+ ('b529bd8164f20d0aa443d4932116841c', 'dc55c076d52bacdf2eefd952946a439d',
+ 'fcfdfeff01020304060708090b0c0d0e',
+ 'ecb-tbl-128: I=43'),
+ ('2e596dcbb2f33d4216a1176d5bd1e456', '711b17b590ffc72b5c8e342b601e8003',
+ '10111213151617181a1b1c1d1f202122',
+ 'ecb-tbl-128: I=44'),
+ ('7274a1ea2b7ee2424e9a0e4673689143', '19983bb0950783a537e1339f4aa21c75',
+ '24252627292a2b2c2e2f303133343536',
+ 'ecb-tbl-128: I=45'),
+ ('ae20020bd4f13e9d90140bee3b5d26af', '3ba7762e15554169c0f4fa39164c410c',
+ '38393a3b3d3e3f40424344454748494a',
+ 'ecb-tbl-128: I=46'),
+ ('baac065da7ac26e855e79c8849d75a02', 'a0564c41245afca7af8aa2e0e588ea89',
+ '4c4d4e4f51525354565758595b5c5d5e',
+ 'ecb-tbl-128: I=47'),
+ ('7c917d8d1d45fab9e2540e28832540cc', '5e36a42a2e099f54ae85ecd92e2381ed',
+ '60616263656667686a6b6c6d6f707172',
+ 'ecb-tbl-128: I=48'),
+ ('bde6f89e16daadb0e847a2a614566a91', '770036f878cd0f6ca2268172f106f2fe',
+ '74757677797a7b7c7e7f808183848586',
+ 'ecb-tbl-128: I=49'),
+ ('c9de163725f1f5be44ebb1db51d07fbc', '7e4e03908b716116443ccf7c94e7c259',
+ '88898a8b8d8e8f90929394959798999a',
+ 'ecb-tbl-128: I=50'),
+ ('3af57a58f0c07dffa669572b521e2b92', '482735a48c30613a242dd494c7f9185d',
+ '9c9d9e9fa1a2a3a4a6a7a8a9abacadae',
+ 'ecb-tbl-128: I=51'),
+ ('3d5ebac306dde4604f1b4fbbbfcdae55', 'b4c0f6c9d4d7079addf9369fc081061d',
+ 'b0b1b2b3b5b6b7b8babbbcbdbfc0c1c2',
+ 'ecb-tbl-128: I=52'),
+ ('c2dfa91bceb76a1183c995020ac0b556', 'd5810fe0509ac53edcd74f89962e6270',
+ 'c4c5c6c7c9cacbcccecfd0d1d3d4d5d6',
+ 'ecb-tbl-128: I=53'),
+ ('c70f54305885e9a0746d01ec56c8596b', '03f17a16b3f91848269ecdd38ebb2165',
+ 'd8d9dadbdddedfe0e2e3e4e5e7e8e9ea',
+ 'ecb-tbl-128: I=54'),
+ ('c4f81b610e98012ce000182050c0c2b2', 'da1248c3180348bad4a93b4d9856c9df',
+ 'ecedeeeff1f2f3f4f6f7f8f9fbfcfdfe',
+ 'ecb-tbl-128: I=55'),
+ ('eaab86b1d02a95d7404eff67489f97d4', '3d10d7b63f3452c06cdf6cce18be0c2c',
+ '00010203050607080a0b0c0d0f101112',
+ 'ecb-tbl-128: I=56'),
+ ('7c55bdb40b88870b52bec3738de82886', '4ab823e7477dfddc0e6789018fcb6258',
+ '14151617191a1b1c1e1f202123242526',
+ 'ecb-tbl-128: I=57'),
+ ('ba6eaa88371ff0a3bd875e3f2a975ce0', 'e6478ba56a77e70cfdaa5c843abde30e',
+ '28292a2b2d2e2f30323334353738393a',
+ 'ecb-tbl-128: I=58'),
+ ('08059130c4c24bd30cf0575e4e0373dc', '1673064895fbeaf7f09c5429ff75772d',
+ '3c3d3e3f41424344464748494b4c4d4e',
+ 'ecb-tbl-128: I=59'),
+ ('9a8eab004ef53093dfcf96f57e7eda82', '4488033ae9f2efd0ca9383bfca1a94e9',
+ '50515253555657585a5b5c5d5f606162',
+ 'ecb-tbl-128: I=60'),
+ ('0745b589e2400c25f117b1d796c28129', '978f3b8c8f9d6f46626cac3c0bcb9217',
+ '64656667696a6b6c6e6f707173747576',
+ 'ecb-tbl-128: I=61'),
+ ('2f1777781216cec3f044f134b1b92bbe', 'e08c8a7e582e15e5527f1d9e2eecb236',
+ '78797a7b7d7e7f80828384858788898a',
+ 'ecb-tbl-128: I=62'),
+ ('353a779ffc541b3a3805d90ce17580fc', 'cec155b76ac5ffda4cf4f9ca91e49a7a',
+ '8c8d8e8f91929394969798999b9c9d9e',
+ 'ecb-tbl-128: I=63'),
+ ('1a1eae4415cefcf08c4ac1c8f68bea8f', 'd5ac7165763225dd2a38cdc6862c29ad',
+ 'a0a1a2a3a5a6a7a8aaabacadafb0b1b2',
+ 'ecb-tbl-128: I=64'),
+ ('e6e7e4e5b0b3b2b5d4d5aaab16111013', '03680fe19f7ce7275452020be70e8204',
+ 'b4b5b6b7b9babbbcbebfc0c1c3c4c5c6',
+ 'ecb-tbl-128: I=65'),
+ ('f8f9fafbfbf8f9e677767170efe0e1e2', '461df740c9781c388e94bb861ceb54f6',
+ 'c8c9cacbcdcecfd0d2d3d4d5d7d8d9da',
+ 'ecb-tbl-128: I=66'),
+ ('63626160a1a2a3a445444b4a75727370', '451bd60367f96483042742219786a074',
+ 'dcdddedfe1e2e3e4e6e7e8e9ebecedee',
+ 'ecb-tbl-128: I=67'),
+ ('717073720605040b2d2c2b2a05fafbf9', 'e4dfa42671a02e57ef173b85c0ea9f2b',
+ 'f0f1f2f3f5f6f7f8fafbfcfdfe010002',
+ 'ecb-tbl-128: I=68'),
+ ('78797a7beae9e8ef3736292891969794', 'ed11b89e76274282227d854700a78b9e',
+ '04050607090a0b0c0e0f101113141516',
+ 'ecb-tbl-128: I=69'),
+ ('838281803231300fdddcdbdaa0afaead', '433946eaa51ea47af33895f2b90b3b75',
+ '18191a1b1d1e1f20222324252728292a',
+ 'ecb-tbl-128: I=70'),
+ ('18191a1bbfbcbdba75747b7a7f78797a', '6bc6d616a5d7d0284a5910ab35022528',
+ '2c2d2e2f31323334363738393b3c3d3e',
+ 'ecb-tbl-128: I=71'),
+ ('848586879b989996a3a2a5a4849b9a99', 'd2a920ecfe919d354b5f49eae9719c98',
+ '40414243454647484a4b4c4d4f505152',
+ 'ecb-tbl-128: I=72'),
+ ('0001020322212027cacbf4f551565754', '3a061b17f6a92885efbd0676985b373d',
+ '54555657595a5b5c5e5f606163646566',
+ 'ecb-tbl-128: I=73'),
+ ('cecfcccdafacadb2515057564a454447', 'fadeec16e33ea2f4688499d157e20d8f',
+ '68696a6b6d6e6f70727374757778797a',
+ 'ecb-tbl-128: I=74'),
+ ('92939091cdcecfc813121d1c80878685', '5cdefede59601aa3c3cda36fa6b1fa13',
+ '7c7d7e7f81828384868788898b8c8d8e',
+ 'ecb-tbl-128: I=75'),
+ ('d2d3d0d16f6c6d6259585f5ed1eeefec', '9574b00039844d92ebba7ee8719265f8',
+ '90919293959697989a9b9c9d9fa0a1a2',
+ 'ecb-tbl-128: I=76'),
+ ('acadaeaf878485820f0e1110d5d2d3d0', '9a9cf33758671787e5006928188643fa',
+ 'a4a5a6a7a9aaabacaeafb0b1b3b4b5b6',
+ 'ecb-tbl-128: I=77'),
+ ('9091929364676619e6e7e0e1757a7b78', '2cddd634c846ba66bb46cbfea4a674f9',
+ 'b8b9babbbdbebfc0c2c3c4c5c7c8c9ca',
+ 'ecb-tbl-128: I=78'),
+ ('babbb8b98a89888f74757a7b92959497', 'd28bae029393c3e7e26e9fafbbb4b98f',
+ 'cccdcecfd1d2d3d4d6d7d8d9dbdcddde',
+ 'ecb-tbl-128: I=79'),
+ ('8d8c8f8e6e6d6c633b3a3d3ccad5d4d7', 'ec27529b1bee0a9ab6a0d73ebc82e9b7',
+ 'e0e1e2e3e5e6e7e8eaebecedeff0f1f2',
+ 'ecb-tbl-128: I=80'),
+ ('86878485010203040808f7f767606162', '3cb25c09472aff6ee7e2b47ccd7ccb17',
+ 'f4f5f6f7f9fafbfcfefe010103040506',
+ 'ecb-tbl-128: I=81'),
+ ('8e8f8c8d656667788a8b8c8d010e0f0c', 'dee33103a7283370d725e44ca38f8fe5',
+ '08090a0b0d0e0f10121314151718191a',
+ 'ecb-tbl-128: I=82'),
+ ('c8c9cacb858687807a7b7475e7e0e1e2', '27f9bcd1aac64bffc11e7815702c1a69',
+ '1c1d1e1f21222324262728292b2c2d2e',
+ 'ecb-tbl-128: I=83'),
+ ('6d6c6f6e5053525d8c8d8a8badd2d3d0', '5df534ffad4ed0749a9988e9849d0021',
+ '30313233353637383a3b3c3d3f404142',
+ 'ecb-tbl-128: I=84'),
+ ('28292a2b393a3b3c0607181903040506', 'a48bee75db04fb60ca2b80f752a8421b',
+ '44454647494a4b4c4e4f505153545556',
+ 'ecb-tbl-128: I=85'),
+ ('a5a4a7a6b0b3b28ddbdadddcbdb2b3b0', '024c8cf70bc86ee5ce03678cb7af45f9',
+ '58595a5b5d5e5f60626364656768696a',
+ 'ecb-tbl-128: I=86'),
+ ('323330316467666130313e3f2c2b2a29', '3c19ac0f8a3a3862ce577831301e166b',
+ '6c6d6e6f71727374767778797b7c7d7e',
+ 'ecb-tbl-128: I=87'),
+ ('27262524080b0a05171611100b141516', 'c5e355b796a57421d59ca6be82e73bca',
+ '80818283858687888a8b8c8d8f909192',
+ 'ecb-tbl-128: I=88'),
+ ('040506074142434435340b0aa3a4a5a6', 'd94033276417abfb05a69d15b6e386e2',
+ '94959697999a9b9c9e9fa0a1a3a4a5a6',
+ 'ecb-tbl-128: I=89'),
+ ('242526271112130c61606766bdb2b3b0', '24b36559ea3a9b9b958fe6da3e5b8d85',
+ 'a8a9aaabadaeafb0b2b3b4b5b7b8b9ba',
+ 'ecb-tbl-128: I=90'),
+ ('4b4a4948252627209e9f9091cec9c8cb', '20fd4feaa0e8bf0cce7861d74ef4cb72',
+ 'bcbdbebfc1c2c3c4c6c7c8c9cbcccdce',
+ 'ecb-tbl-128: I=91'),
+ ('68696a6b6665646b9f9e9998d9e6e7e4', '350e20d5174277b9ec314c501570a11d',
+ 'd0d1d2d3d5d6d7d8dadbdcdddfe0e1e2',
+ 'ecb-tbl-128: I=92'),
+ ('34353637c5c6c7c0f0f1eeef7c7b7a79', '87a29d61b7c604d238fe73045a7efd57',
+ 'e4e5e6e7e9eaebeceeeff0f1f3f4f5f6',
+ 'ecb-tbl-128: I=93'),
+ ('32333031c2c1c13f0d0c0b0a050a0b08', '2c3164c1cc7d0064816bdc0faa362c52',
+ 'f8f9fafbfdfefe00020304050708090a',
+ 'ecb-tbl-128: I=94'),
+ ('cdcccfcebebdbcbbabaaa5a4181f1e1d', '195fe5e8a05a2ed594f6e4400eee10b3',
+ '0c0d0e0f11121314161718191b1c1d1e',
+ 'ecb-tbl-128: I=95'),
+ ('212023223635343ba0a1a6a7445b5a59', 'e4663df19b9a21a5a284c2bd7f905025',
+ '20212223252627282a2b2c2d2f303132',
+ 'ecb-tbl-128: I=96'),
+ ('0e0f0c0da8abaaad2f2e515002050407', '21b88714cfb4e2a933bd281a2c4743fd',
+ '34353637393a3b3c3e3f404143444546',
+ 'ecb-tbl-128: I=97'),
+ ('070605042a2928378e8f8889bdb2b3b0', 'cbfc3980d704fd0fc54378ab84e17870',
+ '48494a4b4d4e4f50525354555758595a',
+ 'ecb-tbl-128: I=98'),
+ ('cbcac9c893909196a9a8a7a6a5a2a3a0', 'bc5144baa48bdeb8b63e22e03da418ef',
+ '5c5d5e5f61626364666768696b6c6d6e',
+ 'ecb-tbl-128: I=99'),
+ ('80818283c1c2c3cc9c9d9a9b0cf3f2f1', '5a1dbaef1ee2984b8395da3bdffa3ccc',
+ '70717273757677787a7b7c7d7f808182',
+ 'ecb-tbl-128: I=100'),
+ ('1213101125262720fafbe4e5b1b6b7b4', 'f0b11cd0729dfcc80cec903d97159574',
+ '84858687898a8b8c8e8f909193949596',
+ 'ecb-tbl-128: I=101'),
+ ('7f7e7d7c3033320d97969190222d2c2f', '9f95314acfddc6d1914b7f19a9cc8209',
+ '98999a9b9d9e9fa0a2a3a4a5a7a8a9aa',
+ 'ecb-tbl-128: I=102'),
+ ('4e4f4c4d484b4a4d81808f8e53545556', '595736f6f0f70914a94e9e007f022519',
+ 'acadaeafb1b2b3b4b6b7b8b9bbbcbdbe',
+ 'ecb-tbl-128: I=103'),
+ ('dcdddedfb0b3b2bd15141312a1bebfbc', '1f19f57892cae586fcdfb4c694deb183',
+ 'c0c1c2c3c5c6c7c8cacbcccdcfd0d1d2',
+ 'ecb-tbl-128: I=104'),
+ ('93929190282b2a2dc4c5fafb92959497', '540700ee1f6f3dab0b3eddf6caee1ef5',
+ 'd4d5d6d7d9dadbdcdedfe0e1e3e4e5e6',
+ 'ecb-tbl-128: I=105'),
+ ('f5f4f7f6c4c7c6d9373631307e717073', '14a342a91019a331687a2254e6626ca2',
+ 'e8e9eaebedeeeff0f2f3f4f5f7f8f9fa',
+ 'ecb-tbl-128: I=106'),
+ ('93929190b6b5b4b364656a6b05020300', '7b25f3c3b2eea18d743ef283140f29ff',
+ 'fcfdfeff01020304060708090b0c0d0e',
+ 'ecb-tbl-128: I=107'),
+ ('babbb8b90d0e0f00a4a5a2a3043b3a39', '46c2587d66e5e6fa7f7ca6411ad28047',
+ '10111213151617181a1b1c1d1f202122',
+ 'ecb-tbl-128: I=108'),
+ ('d8d9dadb7f7c7d7a10110e0f787f7e7d', '09470e72229d954ed5ee73886dfeeba9',
+ '24252627292a2b2c2e2f303133343536',
+ 'ecb-tbl-128: I=109'),
+ ('fefffcfdefeced923b3a3d3c6768696a', 'd77c03de92d4d0d79ef8d4824ef365eb',
+ '38393a3b3d3e3f40424344454748494a',
+ 'ecb-tbl-128: I=110'),
+ ('d6d7d4d58a89888f96979899a5a2a3a0', '1d190219f290e0f1715d152d41a23593',
+ '4c4d4e4f51525354565758595b5c5d5e',
+ 'ecb-tbl-128: I=111'),
+ ('18191a1ba8abaaa5303136379b848586', 'a2cd332ce3a0818769616292e87f757b',
+ '60616263656667686a6b6c6d6f707172',
+ 'ecb-tbl-128: I=112'),
+ ('6b6a6968a4a7a6a1d6d72829b0b7b6b5', 'd54afa6ce60fbf9341a3690e21385102',
+ '74757677797a7b7c7e7f808183848586',
+ 'ecb-tbl-128: I=113'),
+ ('000102038a89889755545352a6a9a8ab', '06e5c364ded628a3f5e05e613e356f46',
+ '88898a8b8d8e8f90929394959798999a',
+ 'ecb-tbl-128: I=114'),
+ ('2d2c2f2eb3b0b1b6b6b7b8b9f2f5f4f7', 'eae63c0e62556dac85d221099896355a',
+ '9c9d9e9fa1a2a3a4a6a7a8a9abacadae',
+ 'ecb-tbl-128: I=115'),
+ ('979695943536373856575051e09f9e9d', '1fed060e2c6fc93ee764403a889985a2',
+ 'b0b1b2b3b5b6b7b8babbbcbdbfc0c1c2',
+ 'ecb-tbl-128: I=116'),
+ ('a4a5a6a7989b9a9db1b0afae7a7d7c7f', 'c25235c1a30fdec1c7cb5c5737b2a588',
+ 'c4c5c6c7c9cacbcccecfd0d1d3d4d5d6',
+ 'ecb-tbl-128: I=117'),
+ ('c1c0c3c2686b6a55a8a9aeafeae5e4e7', '796dbef95147d4d30873ad8b7b92efc0',
+ 'd8d9dadbdddedfe0e2e3e4e5e7e8e9ea',
+ 'ecb-tbl-128: I=118'),
+ ('c1c0c3c2141716118c8d828364636261', 'cbcf0fb34d98d0bd5c22ce37211a46bf',
+ 'ecedeeeff1f2f3f4f6f7f8f9fbfcfdfe',
+ 'ecb-tbl-128: I=119'),
+ ('93929190cccfcec196979091e0fffefd', '94b44da6466126cafa7c7fd09063fc24',
+ '00010203050607080a0b0c0d0f101112',
+ 'ecb-tbl-128: I=120'),
+ ('b4b5b6b7f9fafbfc25241b1a6e69686b', 'd78c5b5ebf9b4dbda6ae506c5074c8fe',
+ '14151617191a1b1c1e1f202123242526',
+ 'ecb-tbl-128: I=121'),
+ ('868784850704051ac7c6c1c08788898a', '6c27444c27204b043812cf8cf95f9769',
+ '28292a2b2d2e2f30323334353738393a',
+ 'ecb-tbl-128: I=122'),
+ ('f4f5f6f7aaa9a8affdfcf3f277707172', 'be94524ee5a2aa50bba8b75f4c0aebcf',
+ '3c3d3e3f41424344464748494b4c4d4e',
+ 'ecb-tbl-128: I=123'),
+ ('d3d2d1d00605040bc3c2c5c43e010003', 'a0aeaae91ba9f31f51aeb3588cf3a39e',
+ '50515253555657585a5b5c5d5f606162',
+ 'ecb-tbl-128: I=124'),
+ ('73727170424140476a6b74750d0a0b08', '275297779c28266ef9fe4c6a13c08488',
+ '64656667696a6b6c6e6f707173747576',
+ 'ecb-tbl-128: I=125'),
+ ('c2c3c0c10a0908f754555253a1aeafac', '86523d92bb8672cb01cf4a77fd725882',
+ '78797a7b7d7e7f80828384858788898a',
+ 'ecb-tbl-128: I=126'),
+ ('6d6c6f6ef8fbfafd82838c8df8fffefd', '4b8327640e9f33322a04dd96fcbf9a36',
+ '8c8d8e8f91929394969798999b9c9d9e',
+ 'ecb-tbl-128: I=127'),
+ ('f5f4f7f684878689a6a7a0a1d2cdcccf', 'ce52af650d088ca559425223f4d32694',
+ 'a0a1a2a3a5a6a7a8aaabacadafb0b1b2',
+ 'ecb-tbl-128: I=128'),
+
+ # ecb_tbl.txt, KEYSIZE=192
+ ('2d33eef2c0430a8a9ebf45e809c40bb6', 'dff4945e0336df4c1c56bc700eff837f',
+ '00010203050607080a0b0c0d0f10111214151617191a1b1c',
+ 'ecb-tbl-192: I=1'),
+ ('6aa375d1fa155a61fb72353e0a5a8756', 'b6fddef4752765e347d5d2dc196d1252',
+ '1e1f20212324252628292a2b2d2e2f30323334353738393a',
+ 'ecb-tbl-192: I=2'),
+ ('bc3736518b9490dcb8ed60eb26758ed4', 'd23684e3d963b3afcf1a114aca90cbd6',
+ '3c3d3e3f41424344464748494b4c4d4e5051525355565758',
+ 'ecb-tbl-192: I=3'),
+ ('aa214402b46cffb9f761ec11263a311e', '3a7ac027753e2a18c2ceab9e17c11fd0',
+ '5a5b5c5d5f60616264656667696a6b6c6e6f707173747576',
+ 'ecb-tbl-192: I=4'),
+ ('02aea86e572eeab66b2c3af5e9a46fd6', '8f6786bd007528ba26603c1601cdd0d8',
+ '78797a7b7d7e7f80828384858788898a8c8d8e8f91929394',
+ 'ecb-tbl-192: I=5'),
+ ('e2aef6acc33b965c4fa1f91c75ff6f36', 'd17d073b01e71502e28b47ab551168b3',
+ '969798999b9c9d9ea0a1a2a3a5a6a7a8aaabacadafb0b1b2',
+ 'ecb-tbl-192: I=6'),
+ ('0659df46427162b9434865dd9499f91d', 'a469da517119fab95876f41d06d40ffa',
+ 'b4b5b6b7b9babbbcbebfc0c1c3c4c5c6c8c9cacbcdcecfd0',
+ 'ecb-tbl-192: I=7'),
+ ('49a44239c748feb456f59c276a5658df', '6091aa3b695c11f5c0b6ad26d3d862ff',
+ 'd2d3d4d5d7d8d9dadcdddedfe1e2e3e4e6e7e8e9ebecedee',
+ 'ecb-tbl-192: I=8'),
+ ('66208f6e9d04525bdedb2733b6a6be37', '70f9e67f9f8df1294131662dc6e69364',
+ 'f0f1f2f3f5f6f7f8fafbfcfdfe01000204050607090a0b0c',
+ 'ecb-tbl-192: I=9'),
+ ('3393f8dfc729c97f5480b950bc9666b0', 'd154dcafad8b207fa5cbc95e9996b559',
+ '0e0f10111314151618191a1b1d1e1f20222324252728292a',
+ 'ecb-tbl-192: I=10'),
+ ('606834c8ce063f3234cf1145325dbd71', '4934d541e8b46fa339c805a7aeb9e5da',
+ '2c2d2e2f31323334363738393b3c3d3e4041424345464748',
+ 'ecb-tbl-192: I=11'),
+ ('fec1c04f529bbd17d8cecfcc4718b17f', '62564c738f3efe186e1a127a0c4d3c61',
+ '4a4b4c4d4f50515254555657595a5b5c5e5f606163646566',
+ 'ecb-tbl-192: I=12'),
+ ('32df99b431ed5dc5acf8caf6dc6ce475', '07805aa043986eb23693e23bef8f3438',
+ '68696a6b6d6e6f70727374757778797a7c7d7e7f81828384',
+ 'ecb-tbl-192: I=13'),
+ ('7fdc2b746f3f665296943b83710d1f82', 'df0b4931038bade848dee3b4b85aa44b',
+ '868788898b8c8d8e90919293959697989a9b9c9d9fa0a1a2',
+ 'ecb-tbl-192: I=14'),
+ ('8fba1510a3c5b87e2eaa3f7a91455ca2', '592d5fded76582e4143c65099309477c',
+ 'a4a5a6a7a9aaabacaeafb0b1b3b4b5b6b8b9babbbdbebfc0',
+ 'ecb-tbl-192: I=15'),
+ ('2c9b468b1c2eed92578d41b0716b223b', 'c9b8d6545580d3dfbcdd09b954ed4e92',
+ 'c2c3c4c5c7c8c9cacccdcecfd1d2d3d4d6d7d8d9dbdcddde',
+ 'ecb-tbl-192: I=16'),
+ ('0a2bbf0efc6bc0034f8a03433fca1b1a', '5dccd5d6eb7c1b42acb008201df707a0',
+ 'e0e1e2e3e5e6e7e8eaebecedeff0f1f2f4f5f6f7f9fafbfc',
+ 'ecb-tbl-192: I=17'),
+ ('25260e1f31f4104d387222e70632504b', 'a2a91682ffeb6ed1d34340946829e6f9',
+ 'fefe01010304050608090a0b0d0e0f10121314151718191a',
+ 'ecb-tbl-192: I=18'),
+ ('c527d25a49f08a5228d338642ae65137', 'e45d185b797000348d9267960a68435d',
+ '1c1d1e1f21222324262728292b2c2d2e3031323335363738',
+ 'ecb-tbl-192: I=19'),
+ ('3b49fc081432f5890d0e3d87e884a69e', '45e060dae5901cda8089e10d4f4c246b',
+ '3a3b3c3d3f40414244454647494a4b4c4e4f505153545556',
+ 'ecb-tbl-192: I=20'),
+ ('d173f9ed1e57597e166931df2754a083', 'f6951afacc0079a369c71fdcff45df50',
+ '58595a5b5d5e5f60626364656768696a6c6d6e6f71727374',
+ 'ecb-tbl-192: I=21'),
+ ('8c2b7cafa5afe7f13562daeae1adede0', '9e95e00f351d5b3ac3d0e22e626ddad6',
+ '767778797b7c7d7e80818283858687888a8b8c8d8f909192',
+ 'ecb-tbl-192: I=22'),
+ ('aaf4ec8c1a815aeb826cab741339532c', '9cb566ff26d92dad083b51fdc18c173c',
+ '94959697999a9b9c9e9fa0a1a3a4a5a6a8a9aaabadaeafb0',
+ 'ecb-tbl-192: I=23'),
+ ('40be8c5d9108e663f38f1a2395279ecf', 'c9c82766176a9b228eb9a974a010b4fb',
+ 'd0d1d2d3d5d6d7d8dadbdcdddfe0e1e2e4e5e6e7e9eaebec',
+ 'ecb-tbl-192: I=24'),
+ ('0c8ad9bc32d43e04716753aa4cfbe351', 'd8e26aa02945881d5137f1c1e1386e88',
+ '2a2b2c2d2f30313234353637393a3b3c3e3f404143444546',
+ 'ecb-tbl-192: I=25'),
+ ('1407b1d5f87d63357c8dc7ebbaebbfee', 'c0e024ccd68ff5ffa4d139c355a77c55',
+ '48494a4b4d4e4f50525354555758595a5c5d5e5f61626364',
+ 'ecb-tbl-192: I=26'),
+ ('e62734d1ae3378c4549e939e6f123416', '0b18b3d16f491619da338640df391d43',
+ '84858687898a8b8c8e8f90919394959698999a9b9d9e9fa0',
+ 'ecb-tbl-192: I=27'),
+ ('5a752cff2a176db1a1de77f2d2cdee41', 'dbe09ac8f66027bf20cb6e434f252efc',
+ 'a2a3a4a5a7a8a9aaacadaeafb1b2b3b4b6b7b8b9bbbcbdbe',
+ 'ecb-tbl-192: I=28'),
+ ('a9c8c3a4eabedc80c64730ddd018cd88', '6d04e5e43c5b9cbe05feb9606b6480fe',
+ 'c0c1c2c3c5c6c7c8cacbcccdcfd0d1d2d4d5d6d7d9dadbdc',
+ 'ecb-tbl-192: I=29'),
+ ('ee9b3dbbdb86180072130834d305999a', 'dd1d6553b96be526d9fee0fbd7176866',
+ '1a1b1c1d1f20212224252627292a2b2c2e2f303133343536',
+ 'ecb-tbl-192: I=30'),
+ ('a7fa8c3586b8ebde7568ead6f634a879', '0260ca7e3f979fd015b0dd4690e16d2a',
+ '38393a3b3d3e3f40424344454748494a4c4d4e4f51525354',
+ 'ecb-tbl-192: I=31'),
+ ('37e0f4a87f127d45ac936fe7ad88c10a', '9893734de10edcc8a67c3b110b8b8cc6',
+ '929394959798999a9c9d9e9fa1a2a3a4a6a7a8a9abacadae',
+ 'ecb-tbl-192: I=32'),
+ ('3f77d8b5d92bac148e4e46f697a535c5', '93b30b750516b2d18808d710c2ee84ef',
+ '464748494b4c4d4e50515253555657585a5b5c5d5f606162',
+ 'ecb-tbl-192: I=33'),
+ ('d25ebb686c40f7e2c4da1014936571ca', '16f65fa47be3cb5e6dfe7c6c37016c0e',
+ '828384858788898a8c8d8e8f91929394969798999b9c9d9e',
+ 'ecb-tbl-192: I=34'),
+ ('4f1c769d1e5b0552c7eca84dea26a549', 'f3847210d5391e2360608e5acb560581',
+ 'a0a1a2a3a5a6a7a8aaabacadafb0b1b2b4b5b6b7b9babbbc',
+ 'ecb-tbl-192: I=35'),
+ ('8548e2f882d7584d0fafc54372b6633a', '8754462cd223366d0753913e6af2643d',
+ 'bebfc0c1c3c4c5c6c8c9cacbcdcecfd0d2d3d4d5d7d8d9da',
+ 'ecb-tbl-192: I=36'),
+ ('87d7a336cb476f177cd2a51af2a62cdf', '1ea20617468d1b806a1fd58145462017',
+ 'dcdddedfe1e2e3e4e6e7e8e9ebecedeef0f1f2f3f5f6f7f8',
+ 'ecb-tbl-192: I=37'),
+ ('03b1feac668c4e485c1065dfc22b44ee', '3b155d927355d737c6be9dda60136e2e',
+ 'fafbfcfdfe01000204050607090a0b0c0e0f101113141516',
+ 'ecb-tbl-192: I=38'),
+ ('bda15e66819fa72d653a6866aa287962', '26144f7b66daa91b6333dbd3850502b3',
+ '18191a1b1d1e1f20222324252728292a2c2d2e2f31323334',
+ 'ecb-tbl-192: I=39'),
+ ('4d0c7a0d2505b80bf8b62ceb12467f0a', 'e4f9a4ab52ced8134c649bf319ebcc90',
+ '363738393b3c3d3e40414243454647484a4b4c4d4f505152',
+ 'ecb-tbl-192: I=40'),
+ ('626d34c9429b37211330986466b94e5f', 'b9ddd29ac6128a6cab121e34a4c62b36',
+ '54555657595a5b5c5e5f60616364656668696a6b6d6e6f70',
+ 'ecb-tbl-192: I=41'),
+ ('333c3e6bf00656b088a17e5ff0e7f60a', '6fcddad898f2ce4eff51294f5eaaf5c9',
+ '727374757778797a7c7d7e7f81828384868788898b8c8d8e',
+ 'ecb-tbl-192: I=42'),
+ ('687ed0cdc0d2a2bc8c466d05ef9d2891', 'c9a6fe2bf4028080bea6f7fc417bd7e3',
+ '90919293959697989a9b9c9d9fa0a1a2a4a5a6a7a9aaabac',
+ 'ecb-tbl-192: I=43'),
+ ('487830e78cc56c1693e64b2a6660c7b6', '6a2026846d8609d60f298a9c0673127f',
+ 'aeafb0b1b3b4b5b6b8b9babbbdbebfc0c2c3c4c5c7c8c9ca',
+ 'ecb-tbl-192: I=44'),
+ ('7a48d6b7b52b29392aa2072a32b66160', '2cb25c005e26efea44336c4c97a4240b',
+ 'cccdcecfd1d2d3d4d6d7d8d9dbdcdddee0e1e2e3e5e6e7e8',
+ 'ecb-tbl-192: I=45'),
+ ('907320e64c8c5314d10f8d7a11c8618d', '496967ab8680ddd73d09a0e4c7dcc8aa',
+ 'eaebecedeff0f1f2f4f5f6f7f9fafbfcfefe010103040506',
+ 'ecb-tbl-192: I=46'),
+ ('b561f2ca2d6e65a4a98341f3ed9ff533', 'd5af94de93487d1f3a8c577cb84a66a4',
+ '08090a0b0d0e0f10121314151718191a1c1d1e1f21222324',
+ 'ecb-tbl-192: I=47'),
+ ('df769380d212792d026f049e2e3e48ef', '84bdac569cae2828705f267cc8376e90',
+ '262728292b2c2d2e30313233353637383a3b3c3d3f404142',
+ 'ecb-tbl-192: I=48'),
+ ('79f374bc445bdabf8fccb8843d6054c6', 'f7401dda5ad5ab712b7eb5d10c6f99b6',
+ '44454647494a4b4c4e4f50515354555658595a5b5d5e5f60',
+ 'ecb-tbl-192: I=49'),
+ ('4e02f1242fa56b05c68dbae8fe44c9d6', '1c9d54318539ebd4c3b5b7e37bf119f0',
+ '626364656768696a6c6d6e6f71727374767778797b7c7d7e',
+ 'ecb-tbl-192: I=50'),
+ ('cf73c93cbff57ac635a6f4ad2a4a1545', 'aca572d65fb2764cffd4a6eca090ea0d',
+ '80818283858687888a8b8c8d8f90919294959697999a9b9c',
+ 'ecb-tbl-192: I=51'),
+ ('9923548e2875750725b886566784c625', '36d9c627b8c2a886a10ccb36eae3dfbb',
+ '9e9fa0a1a3a4a5a6a8a9aaabadaeafb0b2b3b4b5b7b8b9ba',
+ 'ecb-tbl-192: I=52'),
+ ('4888336b723a022c9545320f836a4207', '010edbf5981e143a81d646e597a4a568',
+ 'bcbdbebfc1c2c3c4c6c7c8c9cbcccdced0d1d2d3d5d6d7d8',
+ 'ecb-tbl-192: I=53'),
+ ('f84d9a5561b0608b1160dee000c41ba8', '8db44d538dc20cc2f40f3067fd298e60',
+ 'dadbdcdddfe0e1e2e4e5e6e7e9eaebeceeeff0f1f3f4f5f6',
+ 'ecb-tbl-192: I=54'),
+ ('c23192a0418e30a19b45ae3e3625bf22', '930eb53bc71e6ac4b82972bdcd5aafb3',
+ 'f8f9fafbfdfefe00020304050708090a0c0d0e0f11121314',
+ 'ecb-tbl-192: I=55'),
+ ('b84e0690b28b0025381ad82a15e501a7', '6c42a81edcbc9517ccd89c30c95597b4',
+ '161718191b1c1d1e20212223252627282a2b2c2d2f303132',
+ 'ecb-tbl-192: I=56'),
+ ('acef5e5c108876c4f06269f865b8f0b0', 'da389847ad06df19d76ee119c71e1dd3',
+ '34353637393a3b3c3e3f40414344454648494a4b4d4e4f50',
+ 'ecb-tbl-192: I=57'),
+ ('0f1b3603e0f5ddea4548246153a5e064', 'e018fdae13d3118f9a5d1a647a3f0462',
+ '525354555758595a5c5d5e5f61626364666768696b6c6d6e',
+ 'ecb-tbl-192: I=58'),
+ ('fbb63893450d42b58c6d88cd3c1809e3', '2aa65db36264239d3846180fabdfad20',
+ '70717273757677787a7b7c7d7f80818284858687898a8b8c',
+ 'ecb-tbl-192: I=59'),
+ ('4bef736df150259dae0c91354e8a5f92', '1472163e9a4f780f1ceb44b07ecf4fdb',
+ '8e8f90919394959698999a9b9d9e9fa0a2a3a4a5a7a8a9aa',
+ 'ecb-tbl-192: I=60'),
+ ('7d2d46242056ef13d3c3fc93c128f4c7', 'c8273fdc8f3a9f72e91097614b62397c',
+ 'acadaeafb1b2b3b4b6b7b8b9bbbcbdbec0c1c2c3c5c6c7c8',
+ 'ecb-tbl-192: I=61'),
+ ('e9c1ba2df415657a256edb33934680fd', '66c8427dcd733aaf7b3470cb7d976e3f',
+ 'cacbcccdcfd0d1d2d4d5d6d7d9dadbdcdedfe0e1e3e4e5e6',
+ 'ecb-tbl-192: I=62'),
+ ('e23ee277b0aa0a1dfb81f7527c3514f1', '146131cb17f1424d4f8da91e6f80c1d0',
+ 'e8e9eaebedeeeff0f2f3f4f5f7f8f9fafcfdfeff01020304',
+ 'ecb-tbl-192: I=63'),
+ ('3e7445b0b63caaf75e4a911e12106b4c', '2610d0ad83659081ae085266a88770dc',
+ '060708090b0c0d0e10111213151617181a1b1c1d1f202122',
+ 'ecb-tbl-192: I=64'),
+ ('767774752023222544455a5be6e1e0e3', '38a2b5a974b0575c5d733917fb0d4570',
+ '24252627292a2b2c2e2f30313334353638393a3b3d3e3f40',
+ 'ecb-tbl-192: I=65'),
+ ('72737475717e7f7ce9e8ebea696a6b6c', 'e21d401ebc60de20d6c486e4f39a588b',
+ '424344454748494a4c4d4e4f51525354565758595b5c5d5e',
+ 'ecb-tbl-192: I=66'),
+ ('dfdedddc25262728c9c8cfcef1eeefec', 'e51d5f88c670b079c0ca1f0c2c4405a2',
+ '60616263656667686a6b6c6d6f70717274757677797a7b7c',
+ 'ecb-tbl-192: I=67'),
+ ('fffe0100707776755f5e5d5c7675746b', '246a94788a642fb3d1b823c8762380c8',
+ '7e7f80818384858688898a8b8d8e8f90929394959798999a',
+ 'ecb-tbl-192: I=68'),
+ ('e0e1e2e3424140479f9e9190292e2f2c', 'b80c391c5c41a4c3b30c68e0e3d7550f',
+ '9c9d9e9fa1a2a3a4a6a7a8a9abacadaeb0b1b2b3b5b6b7b8',
+ 'ecb-tbl-192: I=69'),
+ ('2120272690efeeed3b3a39384e4d4c4b', 'b77c4754fc64eb9a1154a9af0bb1f21c',
+ 'babbbcbdbfc0c1c2c4c5c6c7c9cacbcccecfd0d1d3d4d5d6',
+ 'ecb-tbl-192: I=70'),
+ ('ecedeeef5350516ea1a0a7a6a3acadae', 'fb554de520d159a06bf219fc7f34a02f',
+ 'd8d9dadbdddedfe0e2e3e4e5e7e8e9eaecedeeeff1f2f3f4',
+ 'ecb-tbl-192: I=71'),
+ ('32333c3d25222320e9e8ebeacecdccc3', 'a89fba152d76b4927beed160ddb76c57',
+ 'f6f7f8f9fbfcfdfe00010203050607080a0b0c0d0f101112',
+ 'ecb-tbl-192: I=72'),
+ ('40414243626160678a8bb4b511161714', '5676eab4a98d2e8473b3f3d46424247c',
+ '14151617191a1b1c1e1f20212324252628292a2b2d2e2f30',
+ 'ecb-tbl-192: I=73'),
+ ('94959293f5fafbf81f1e1d1c7c7f7e79', '4e8f068bd7ede52a639036ec86c33568',
+ '323334353738393a3c3d3e3f41424344464748494b4c4d4e',
+ 'ecb-tbl-192: I=74'),
+ ('bebfbcbd191a1b14cfcec9c8546b6a69', 'f0193c4d7aff1791ee4c07eb4a1824fc',
+ '50515253555657585a5b5c5d5f60616264656667696a6b6c',
+ 'ecb-tbl-192: I=75'),
+ ('2c2d3233898e8f8cbbbab9b8333031ce', 'ac8686eeca9ba761afe82d67b928c33f',
+ '6e6f70717374757678797a7b7d7e7f80828384858788898a',
+ 'ecb-tbl-192: I=76'),
+ ('84858687bfbcbdba37363938fdfafbf8', '5faf8573e33b145b6a369cd3606ab2c9',
+ '8c8d8e8f91929394969798999b9c9d9ea0a1a2a3a5a6a7a8',
+ 'ecb-tbl-192: I=77'),
+ ('828384857669686b909192930b08090e', '31587e9944ab1c16b844ecad0df2e7da',
+ 'aaabacadafb0b1b2b4b5b6b7b9babbbcbebfc0c1c3c4c5c6',
+ 'ecb-tbl-192: I=78'),
+ ('bebfbcbd9695948b707176779e919093', 'd017fecd91148aba37f6f3068aa67d8a',
+ 'c8c9cacbcdcecfd0d2d3d4d5d7d8d9dadcdddedfe1e2e3e4',
+ 'ecb-tbl-192: I=79'),
+ ('8b8a85846067666521202322d0d3d2dd', '788ef2f021a73cba2794b616078a8500',
+ 'e6e7e8e9ebecedeef0f1f2f3f5f6f7f8fafbfcfdfe010002',
+ 'ecb-tbl-192: I=80'),
+ ('76777475f1f2f3f4f8f9e6e777707172', '5d1ef20dced6bcbc12131ac7c54788aa',
+ '04050607090a0b0c0e0f10111314151618191a1b1d1e1f20',
+ 'ecb-tbl-192: I=81'),
+ ('a4a5a2a34f404142b4b5b6b727242522', 'b3c8cf961faf9ea05fdde6d1e4d8f663',
+ '222324252728292a2c2d2e2f31323334363738393b3c3d3e',
+ 'ecb-tbl-192: I=82'),
+ ('94959697e1e2e3ec16171011839c9d9e', '143075c70605861c7fac6526199e459f',
+ '40414243454647484a4b4c4d4f50515254555657595a5b5c',
+ 'ecb-tbl-192: I=83'),
+ ('03023d3c06010003dedfdcddfffcfde2', 'a5ae12eade9a87268d898bfc8fc0252a',
+ '5e5f60616364656668696a6b6d6e6f70727374757778797a',
+ 'ecb-tbl-192: I=84'),
+ ('10111213f1f2f3f4cecfc0c1dbdcddde', '0924f7cf2e877a4819f5244a360dcea9',
+ '7c7d7e7f81828384868788898b8c8d8e9091929395969798',
+ 'ecb-tbl-192: I=85'),
+ ('67666160724d4c4f1d1c1f1e73707176', '3d9e9635afcc3e291cc7ab3f27d1c99a',
+ '9a9b9c9d9fa0a1a2a4a5a6a7a9aaabacaeafb0b1b3b4b5b6',
+ 'ecb-tbl-192: I=86'),
+ ('e6e7e4e5a8abaad584858283909f9e9d', '9d80feebf87510e2b8fb98bb54fd788c',
+ 'b8b9babbbdbebfc0c2c3c4c5c7c8c9cacccdcecfd1d2d3d4',
+ 'ecb-tbl-192: I=87'),
+ ('71707f7e565150537d7c7f7e6162636c', '5f9d1a082a1a37985f174002eca01309',
+ 'd6d7d8d9dbdcdddee0e1e2e3e5e6e7e8eaebecedeff0f1f2',
+ 'ecb-tbl-192: I=88'),
+ ('64656667212223245555aaaa03040506', 'a390ebb1d1403930184a44b4876646e4',
+ 'f4f5f6f7f9fafbfcfefe01010304050608090a0b0d0e0f10',
+ 'ecb-tbl-192: I=89'),
+ ('9e9f9899aba4a5a6cfcecdcc2b28292e', '700fe918981c3195bb6c4bcb46b74e29',
+ '121314151718191a1c1d1e1f21222324262728292b2c2d2e',
+ 'ecb-tbl-192: I=90'),
+ ('c7c6c5c4d1d2d3dc626364653a454447', '907984406f7bf2d17fb1eb15b673d747',
+ '30313233353637383a3b3c3d3f40414244454647494a4b4c',
+ 'ecb-tbl-192: I=91'),
+ ('f6f7e8e9e0e7e6e51d1c1f1e5b585966', 'c32a956dcfc875c2ac7c7cc8b8cc26e1',
+ '4e4f50515354555658595a5b5d5e5f60626364656768696a',
+ 'ecb-tbl-192: I=92'),
+ ('bcbdbebf5d5e5f5868696667f4f3f2f1', '02646e2ebfa9b820cf8424e9b9b6eb51',
+ '6c6d6e6f71727374767778797b7c7d7e8081828385868788',
+ 'ecb-tbl-192: I=93'),
+ ('40414647b0afaead9b9a99989b98999e', '621fda3a5bbd54c6d3c685816bd4ead8',
+ '8a8b8c8d8f90919294959697999a9b9c9e9fa0a1a3a4a5a6',
+ 'ecb-tbl-192: I=94'),
+ ('69686b6a0201001f0f0e0908b4bbbab9', 'd4e216040426dfaf18b152469bc5ac2f',
+ 'a8a9aaabadaeafb0b2b3b4b5b7b8b9babcbdbebfc1c2c3c4',
+ 'ecb-tbl-192: I=95'),
+ ('c7c6c9c8d8dfdedd5a5b5859bebdbcb3', '9d0635b9d33b6cdbd71f5d246ea17cc8',
+ 'c6c7c8c9cbcccdced0d1d2d3d5d6d7d8dadbdcdddfe0e1e2',
+ 'ecb-tbl-192: I=96'),
+ ('dedfdcdd787b7a7dfffee1e0b2b5b4b7', '10abad1bd9bae5448808765583a2cc1a',
+ 'e4e5e6e7e9eaebeceeeff0f1f3f4f5f6f8f9fafbfdfefe00',
+ 'ecb-tbl-192: I=97'),
+ ('4d4c4b4a606f6e6dd0d1d2d3fbf8f9fe', '6891889e16544e355ff65a793c39c9a8',
+ '020304050708090a0c0d0e0f11121314161718191b1c1d1e',
+ 'ecb-tbl-192: I=98'),
+ ('b7b6b5b4d7d4d5dae5e4e3e2e1fefffc', 'cc735582e68072c163cd9ddf46b91279',
+ '20212223252627282a2b2c2d2f30313234353637393a3b3c',
+ 'ecb-tbl-192: I=99'),
+ ('cecfb0b1f7f0f1f2aeafacad3e3d3c23', 'c5c68b9aeeb7f878df578efa562f9574',
+ '3e3f40414344454648494a4b4d4e4f50525354555758595a',
+ 'ecb-tbl-192: I=100'),
+ ('cacbc8c9cdcecfc812131c1d494e4f4c', '5f4764395a667a47d73452955d0d2ce8',
+ '5c5d5e5f61626364666768696b6c6d6e7071727375767778',
+ 'ecb-tbl-192: I=101'),
+ ('9d9c9b9ad22d2c2fb1b0b3b20c0f0e09', '701448331f66106cefddf1eb8267c357',
+ '7a7b7c7d7f80818284858687898a8b8c8e8f909193949596',
+ 'ecb-tbl-192: I=102'),
+ ('7a7b787964676659959493924f404142', 'cb3ee56d2e14b4e1941666f13379d657',
+ '98999a9b9d9e9fa0a2a3a4a5a7a8a9aaacadaeafb1b2b3b4',
+ 'ecb-tbl-192: I=103'),
+ ('aaaba4a5cec9c8cb1f1e1d1caba8a9a6', '9fe16efd18ab6e1981191851fedb0764',
+ 'b6b7b8b9bbbcbdbec0c1c2c3c5c6c7c8cacbcccdcfd0d1d2',
+ 'ecb-tbl-192: I=104'),
+ ('93929190282b2a2dc4c5fafb92959497', '3dc9ba24e1b223589b147adceb4c8e48',
+ 'd4d5d6d7d9dadbdcdedfe0e1e3e4e5e6e8e9eaebedeeeff0',
+ 'ecb-tbl-192: I=105'),
+ ('efeee9e8ded1d0d339383b3a888b8a8d', '1c333032682e7d4de5e5afc05c3e483c',
+ 'f2f3f4f5f7f8f9fafcfdfeff01020304060708090b0c0d0e',
+ 'ecb-tbl-192: I=106'),
+ ('7f7e7d7ca2a1a0af78797e7f112e2f2c', 'd593cc99a95afef7e92038e05a59d00a',
+ '10111213151617181a1b1c1d1f20212224252627292a2b2c',
+ 'ecb-tbl-192: I=107'),
+ ('84859a9b2b2c2d2e868784852625245b', '51e7f96f53b4353923452c222134e1ec',
+ '2e2f30313334353638393a3b3d3e3f40424344454748494a',
+ 'ecb-tbl-192: I=108'),
+ ('b0b1b2b3070405026869666710171615', '4075b357a1a2b473400c3b25f32f81a4',
+ '4c4d4e4f51525354565758595b5c5d5e6061626365666768',
+ 'ecb-tbl-192: I=109'),
+ ('acadaaabbda2a3a00d0c0f0e595a5b5c', '302e341a3ebcd74f0d55f61714570284',
+ '6a6b6c6d6f70717274757677797a7b7c7e7f808183848586',
+ 'ecb-tbl-192: I=110'),
+ ('121310115655544b5253545569666764', '57abdd8231280da01c5042b78cf76522',
+ '88898a8b8d8e8f90929394959798999a9c9d9e9fa1a2a3a4',
+ 'ecb-tbl-192: I=111'),
+ ('dedfd0d166616063eaebe8e94142434c', '17f9ea7eea17ac1adf0e190fef799e92',
+ 'a6a7a8a9abacadaeb0b1b2b3b5b6b7b8babbbcbdbfc0c1c2',
+ 'ecb-tbl-192: I=112'),
+ ('dbdad9d81417161166677879e0e7e6e5', '2e1bdd563dd87ee5c338dd6d098d0a7a',
+ 'c4c5c6c7c9cacbcccecfd0d1d3d4d5d6d8d9dadbdddedfe0',
+ 'ecb-tbl-192: I=113'),
+ ('6a6b6c6de0efeeed2b2a2928c0c3c2c5', 'eb869996e6f8bfb2bfdd9e0c4504dbb2',
+ 'e2e3e4e5e7e8e9eaecedeeeff1f2f3f4f6f7f8f9fbfcfdfe',
+ 'ecb-tbl-192: I=114'),
+ ('b1b0b3b21714151a1a1b1c1d5649484b', 'c2e01549e9decf317468b3e018c61ba8',
+ '00010203050607080a0b0c0d0f10111214151617191a1b1c',
+ 'ecb-tbl-192: I=115'),
+ ('39380706a3a4a5a6c4c5c6c77271706f', '8da875d033c01dd463b244a1770f4a22',
+ '1e1f20212324252628292a2b2d2e2f30323334353738393a',
+ 'ecb-tbl-192: I=116'),
+ ('5c5d5e5f1013121539383736e2e5e4e7', '8ba0dcf3a186844f026d022f8839d696',
+ '3c3d3e3f41424344464748494b4c4d4e5051525355565758',
+ 'ecb-tbl-192: I=117'),
+ ('43424544ead5d4d72e2f2c2d64676661', 'e9691ff9a6cc6970e51670a0fd5b88c1',
+ '5a5b5c5d5f60616264656667696a6b6c6e6f707173747576',
+ 'ecb-tbl-192: I=118'),
+ ('55545756989b9a65f8f9feff18171615', 'f2baec06faeed30f88ee63ba081a6e5b',
+ '78797a7b7d7e7f80828384858788898a8c8d8e8f91929394',
+ 'ecb-tbl-192: I=119'),
+ ('05040b0a525554573c3d3e3f4a494847', '9c39d4c459ae5753394d6094adc21e78',
+ '969798999b9c9d9ea0a1a2a3a5a6a7a8aaabacadafb0b1b2',
+ 'ecb-tbl-192: I=120'),
+ ('14151617595a5b5c8584fbfa8e89888b', '6345b532a11904502ea43ba99c6bd2b2',
+ 'b4b5b6b7b9babbbcbebfc0c1c3c4c5c6c8c9cacbcdcecfd0',
+ 'ecb-tbl-192: I=121'),
+ ('7c7d7a7bfdf2f3f029282b2a51525354', '5ffae3061a95172e4070cedce1e428c8',
+ 'd2d3d4d5d7d8d9dadcdddedfe1e2e3e4e6e7e8e9ebecedee',
+ 'ecb-tbl-192: I=122'),
+ ('38393a3b1e1d1c1341404746c23d3c3e', '0a4566be4cdf9adce5dec865b5ab34cd',
+ 'f0f1f2f3f5f6f7f8fafbfcfdfe01000204050607090a0b0c',
+ 'ecb-tbl-192: I=123'),
+ ('8d8c939240474645818083827c7f7e41', 'ca17fcce79b7404f2559b22928f126fb',
+ '0e0f10111314151618191a1b1d1e1f20222324252728292a',
+ 'ecb-tbl-192: I=124'),
+ ('3b3a39381a19181f32333c3d45424340', '97ca39b849ed73a6470a97c821d82f58',
+ '2c2d2e2f31323334363738393b3c3d3e4041424345464748',
+ 'ecb-tbl-192: I=125'),
+ ('f0f1f6f738272625828380817f7c7d7a', '8198cb06bc684c6d3e9b7989428dcf7a',
+ '4a4b4c4d4f50515254555657595a5b5c5e5f606163646566',
+ 'ecb-tbl-192: I=126'),
+ ('89888b8a0407061966676061141b1a19', 'f53c464c705ee0f28d9a4c59374928bd',
+ '68696a6b6d6e6f70727374757778797a7c7d7e7f81828384',
+ 'ecb-tbl-192: I=127'),
+ ('d3d2dddcaaadacaf9c9d9e9fe8ebeae5', '9adb3d4cca559bb98c3e2ed73dbf1154',
+ '868788898b8c8d8e90919293959697989a9b9c9d9fa0a1a2',
+ 'ecb-tbl-192: I=128'),
+
+ # ecb_tbl.txt, KEYSIZE=256
+ ('834eadfccac7e1b30664b1aba44815ab', '1946dabf6a03a2a2c3d0b05080aed6fc',
+ '00010203050607080a0b0c0d0f10111214151617191a1b1c1e1f202123242526',
+ 'ecb-tbl-256: I=1'),
+ ('d9dc4dba3021b05d67c0518f72b62bf1', '5ed301d747d3cc715445ebdec62f2fb4',
+ '28292a2b2d2e2f30323334353738393a3c3d3e3f41424344464748494b4c4d4e',
+ 'ecb-tbl-256: I=2'),
+ ('a291d86301a4a739f7392173aa3c604c', '6585c8f43d13a6beab6419fc5935b9d0',
+ '50515253555657585a5b5c5d5f60616264656667696a6b6c6e6f707173747576',
+ 'ecb-tbl-256: I=3'),
+ ('4264b2696498de4df79788a9f83e9390', '2a5b56a596680fcc0e05f5e0f151ecae',
+ '78797a7b7d7e7f80828384858788898a8c8d8e8f91929394969798999b9c9d9e',
+ 'ecb-tbl-256: I=4'),
+ ('ee9932b3721804d5a83ef5949245b6f6', 'f5d6ff414fd2c6181494d20c37f2b8c4',
+ 'a0a1a2a3a5a6a7a8aaabacadafb0b1b2b4b5b6b7b9babbbcbebfc0c1c3c4c5c6',
+ 'ecb-tbl-256: I=5'),
+ ('e6248f55c5fdcbca9cbbb01c88a2ea77', '85399c01f59fffb5204f19f8482f00b8',
+ 'c8c9cacbcdcecfd0d2d3d4d5d7d8d9dadcdddedfe1e2e3e4e6e7e8e9ebecedee',
+ 'ecb-tbl-256: I=6'),
+ ('b8358e41b9dff65fd461d55a99266247', '92097b4c88a041ddf98144bc8d22e8e7',
+ 'f0f1f2f3f5f6f7f8fafbfcfdfe01000204050607090a0b0c0e0f101113141516',
+ 'ecb-tbl-256: I=7'),
+ ('f0e2d72260af58e21e015ab3a4c0d906', '89bd5b73b356ab412aef9f76cea2d65c',
+ '18191a1b1d1e1f20222324252728292a2c2d2e2f31323334363738393b3c3d3e',
+ 'ecb-tbl-256: I=8'),
+ ('475b8b823ce8893db3c44a9f2a379ff7', '2536969093c55ff9454692f2fac2f530',
+ '40414243454647484a4b4c4d4f50515254555657595a5b5c5e5f606163646566',
+ 'ecb-tbl-256: I=9'),
+ ('688f5281945812862f5f3076cf80412f', '07fc76a872843f3f6e0081ee9396d637',
+ '68696a6b6d6e6f70727374757778797a7c7d7e7f81828384868788898b8c8d8e',
+ 'ecb-tbl-256: I=10'),
+ ('08d1d2bc750af553365d35e75afaceaa', 'e38ba8ec2aa741358dcc93e8f141c491',
+ '90919293959697989a9b9c9d9fa0a1a2a4a5a6a7a9aaabacaeafb0b1b3b4b5b6',
+ 'ecb-tbl-256: I=11'),
+ ('8707121f47cc3efceca5f9a8474950a1', 'd028ee23e4a89075d0b03e868d7d3a42',
+ 'b8b9babbbdbebfc0c2c3c4c5c7c8c9cacccdcecfd1d2d3d4d6d7d8d9dbdcddde',
+ 'ecb-tbl-256: I=12'),
+ ('e51aa0b135dba566939c3b6359a980c5', '8cd9423dfc459e547155c5d1d522e540',
+ 'e0e1e2e3e5e6e7e8eaebecedeff0f1f2f4f5f6f7f9fafbfcfefe010103040506',
+ 'ecb-tbl-256: I=13'),
+ ('069a007fc76a459f98baf917fedf9521', '080e9517eb1677719acf728086040ae3',
+ '08090a0b0d0e0f10121314151718191a1c1d1e1f21222324262728292b2c2d2e',
+ 'ecb-tbl-256: I=14'),
+ ('726165c1723fbcf6c026d7d00b091027', '7c1700211a3991fc0ecded0ab3e576b0',
+ '30313233353637383a3b3c3d3f40414244454647494a4b4c4e4f505153545556',
+ 'ecb-tbl-256: I=15'),
+ ('d7c544de91d55cfcde1f84ca382200ce', 'dabcbcc855839251db51e224fbe87435',
+ '58595a5b5d5e5f60626364656768696a6c6d6e6f71727374767778797b7c7d7e',
+ 'ecb-tbl-256: I=16'),
+ ('fed3c9a161b9b5b2bd611b41dc9da357', '68d56fad0406947a4dd27a7448c10f1d',
+ '80818283858687888a8b8c8d8f90919294959697999a9b9c9e9fa0a1a3a4a5a6',
+ 'ecb-tbl-256: I=17'),
+ ('4f634cdc6551043409f30b635832cf82', 'da9a11479844d1ffee24bbf3719a9925',
+ 'a8a9aaabadaeafb0b2b3b4b5b7b8b9babcbdbebfc1c2c3c4c6c7c8c9cbcccdce',
+ 'ecb-tbl-256: I=18'),
+ ('109ce98db0dfb36734d9f3394711b4e6', '5e4ba572f8d23e738da9b05ba24b8d81',
+ 'd0d1d2d3d5d6d7d8dadbdcdddfe0e1e2e4e5e6e7e9eaebeceeeff0f1f3f4f5f6',
+ 'ecb-tbl-256: I=19'),
+ ('4ea6dfaba2d8a02ffdffa89835987242', 'a115a2065d667e3f0b883837a6e903f8',
+ '70717273757677787a7b7c7d7f80818284858687898a8b8c8e8f909193949596',
+ 'ecb-tbl-256: I=20'),
+ ('5ae094f54af58e6e3cdbf976dac6d9ef', '3e9e90dc33eac2437d86ad30b137e66e',
+ '98999a9b9d9e9fa0a2a3a4a5a7a8a9aaacadaeafb1b2b3b4b6b7b8b9bbbcbdbe',
+ 'ecb-tbl-256: I=21'),
+ ('764d8e8e0f29926dbe5122e66354fdbe', '01ce82d8fbcdae824cb3c48e495c3692',
+ 'c0c1c2c3c5c6c7c8cacbcccdcfd0d1d2d4d5d6d7d9dadbdcdedfe0e1e3e4e5e6',
+ 'ecb-tbl-256: I=22'),
+ ('3f0418f888cdf29a982bf6b75410d6a9', '0c9cff163ce936faaf083cfd3dea3117',
+ 'e8e9eaebedeeeff0f2f3f4f5f7f8f9fafcfdfeff01020304060708090b0c0d0e',
+ 'ecb-tbl-256: I=23'),
+ ('e4a3e7cb12cdd56aa4a75197a9530220', '5131ba9bd48f2bba85560680df504b52',
+ '10111213151617181a1b1c1d1f20212224252627292a2b2c2e2f303133343536',
+ 'ecb-tbl-256: I=24'),
+ ('211677684aac1ec1a160f44c4ebf3f26', '9dc503bbf09823aec8a977a5ad26ccb2',
+ '38393a3b3d3e3f40424344454748494a4c4d4e4f51525354565758595b5c5d5e',
+ 'ecb-tbl-256: I=25'),
+ ('d21e439ff749ac8f18d6d4b105e03895', '9a6db0c0862e506a9e397225884041d7',
+ '60616263656667686a6b6c6d6f70717274757677797a7b7c7e7f808183848586',
+ 'ecb-tbl-256: I=26'),
+ ('d9f6ff44646c4725bd4c0103ff5552a7', '430bf9570804185e1ab6365fc6a6860c',
+ '88898a8b8d8e8f90929394959798999a9c9d9e9fa1a2a3a4a6a7a8a9abacadae',
+ 'ecb-tbl-256: I=27'),
+ ('0b1256c2a00b976250cfc5b0c37ed382', '3525ebc02f4886e6a5a3762813e8ce8a',
+ 'b0b1b2b3b5b6b7b8babbbcbdbfc0c1c2c4c5c6c7c9cacbcccecfd0d1d3d4d5d6',
+ 'ecb-tbl-256: I=28'),
+ ('b056447ffc6dc4523a36cc2e972a3a79', '07fa265c763779cce224c7bad671027b',
+ 'd8d9dadbdddedfe0e2e3e4e5e7e8e9eaecedeeeff1f2f3f4f6f7f8f9fbfcfdfe',
+ 'ecb-tbl-256: I=29'),
+ ('5e25ca78f0de55802524d38da3fe4456', 'e8b72b4e8be243438c9fff1f0e205872',
+ '00010203050607080a0b0c0d0f10111214151617191a1b1c1e1f202123242526',
+ 'ecb-tbl-256: I=30'),
+ ('a5bcf4728fa5eaad8567c0dc24675f83', '109d4f999a0e11ace1f05e6b22cbcb50',
+ '28292a2b2d2e2f30323334353738393a3c3d3e3f41424344464748494b4c4d4e',
+ 'ecb-tbl-256: I=31'),
+ ('814e59f97ed84646b78b2ca022e9ca43', '45a5e8d4c3ed58403ff08d68a0cc4029',
+ '50515253555657585a5b5c5d5f60616264656667696a6b6c6e6f707173747576',
+ 'ecb-tbl-256: I=32'),
+ ('15478beec58f4775c7a7f5d4395514d7', '196865964db3d417b6bd4d586bcb7634',
+ '78797a7b7d7e7f80828384858788898a8c8d8e8f91929394969798999b9c9d9e',
+ 'ecb-tbl-256: I=33'),
+ ('253548ffca461c67c8cbc78cd59f4756', '60436ad45ac7d30d99195f815d98d2ae',
+ 'a0a1a2a3a5a6a7a8aaabacadafb0b1b2b4b5b6b7b9babbbcbebfc0c1c3c4c5c6',
+ 'ecb-tbl-256: I=34'),
+ ('fd7ad8d73b9b0f8cc41600640f503d65', 'bb07a23f0b61014b197620c185e2cd75',
+ 'c8c9cacbcdcecfd0d2d3d4d5d7d8d9dadcdddedfe1e2e3e4e6e7e8e9ebecedee',
+ 'ecb-tbl-256: I=35'),
+ ('06199de52c6cbf8af954cd65830bcd56', '5bc0b2850129c854423aff0751fe343b',
+ 'f0f1f2f3f5f6f7f8fafbfcfdfe01000204050607090a0b0c0e0f101113141516',
+ 'ecb-tbl-256: I=36'),
+ ('f17c4ffe48e44c61bd891e257e725794', '7541a78f96738e6417d2a24bd2beca40',
+ '18191a1b1d1e1f20222324252728292a2c2d2e2f31323334363738393b3c3d3e',
+ 'ecb-tbl-256: I=37'),
+ ('9a5b4a402a3e8a59be6bf5cd8154f029', 'b0a303054412882e464591f1546c5b9e',
+ '40414243454647484a4b4c4d4f50515254555657595a5b5c5e5f606163646566',
+ 'ecb-tbl-256: I=38'),
+ ('79bd40b91a7e07dc939d441782ae6b17', '778c06d8a355eeee214fcea14b4e0eef',
+ '68696a6b6d6e6f70727374757778797a7c7d7e7f81828384868788898b8c8d8e',
+ 'ecb-tbl-256: I=39'),
+ ('d8ceaaf8976e5fbe1012d8c84f323799', '09614206d15cbace63227d06db6beebb',
+ '90919293959697989a9b9c9d9fa0a1a2a4a5a6a7a9aaabacaeafb0b1b3b4b5b6',
+ 'ecb-tbl-256: I=40'),
+ ('3316e2751e2e388b083da23dd6ac3fbe', '41b97fb20e427a9fdbbb358d9262255d',
+ 'b8b9babbbdbebfc0c2c3c4c5c7c8c9cacccdcecfd1d2d3d4d6d7d8d9dbdcddde',
+ 'ecb-tbl-256: I=41'),
+ ('8b7cfbe37de7dca793521819242c5816', 'c1940f703d845f957652c2d64abd7adf',
+ 'e0e1e2e3e5e6e7e8eaebecedeff0f1f2f4f5f6f7f9fafbfcfefe010103040506',
+ 'ecb-tbl-256: I=42'),
+ ('f23f033c0eebf8ec55752662fd58ce68', 'd2d44fcdae5332343366db297efcf21b',
+ '08090a0b0d0e0f10121314151718191a1c1d1e1f21222324262728292b2c2d2e',
+ 'ecb-tbl-256: I=43'),
+ ('59eb34f6c8bdbacc5fc6ad73a59a1301', 'ea8196b79dbe167b6aa9896e287eed2b',
+ '30313233353637383a3b3c3d3f40414244454647494a4b4c4e4f505153545556',
+ 'ecb-tbl-256: I=44'),
+ ('dcde8b6bd5cf7cc22d9505e3ce81261a', 'd6b0b0c4ba6c7dbe5ed467a1e3f06c2d',
+ '58595a5b5d5e5f60626364656768696a6c6d6e6f71727374767778797b7c7d7e',
+ 'ecb-tbl-256: I=45'),
+ ('e33cf7e524fed781e7042ff9f4b35dc7', 'ec51eb295250c22c2fb01816fb72bcae',
+ '80818283858687888a8b8c8d8f90919294959697999a9b9c9e9fa0a1a3a4a5a6',
+ 'ecb-tbl-256: I=46'),
+ ('27963c8facdf73062867d164df6d064c', 'aded6630a07ce9c7408a155d3bd0d36f',
+ 'a8a9aaabadaeafb0b2b3b4b5b7b8b9babcbdbebfc1c2c3c4c6c7c8c9cbcccdce',
+ 'ecb-tbl-256: I=47'),
+ ('77b1ce386b551b995f2f2a1da994eef8', '697c9245b9937f32f5d1c82319f0363a',
+ 'd0d1d2d3d5d6d7d8dadbdcdddfe0e1e2e4e5e6e7e9eaebeceeeff0f1f3f4f5f6',
+ 'ecb-tbl-256: I=48'),
+ ('f083388b013679efcf0bb9b15d52ae5c', 'aad5ad50c6262aaec30541a1b7b5b19c',
+ 'f8f9fafbfdfefe00020304050708090a0c0d0e0f11121314161718191b1c1d1e',
+ 'ecb-tbl-256: I=49'),
+ ('c5009e0dab55db0abdb636f2600290c8', '7d34b893855341ec625bd6875ac18c0d',
+ '20212223252627282a2b2c2d2f30313234353637393a3b3c3e3f404143444546',
+ 'ecb-tbl-256: I=50'),
+ ('7804881e26cd532d8514d3683f00f1b9', '7ef05105440f83862f5d780e88f02b41',
+ '48494a4b4d4e4f50525354555758595a5c5d5e5f61626364666768696b6c6d6e',
+ 'ecb-tbl-256: I=51'),
+ ('46cddcd73d1eb53e675ca012870a92a3', 'c377c06403382061af2c9c93a8e70df6',
+ '70717273757677787a7b7c7d7f80818284858687898a8b8c8e8f909193949596',
+ 'ecb-tbl-256: I=52'),
+ ('a9fb44062bb07fe130a8e8299eacb1ab', '1dbdb3ffdc052dacc83318853abc6de5',
+ '98999a9b9d9e9fa0a2a3a4a5a7a8a9aaacadaeafb1b2b3b4b6b7b8b9bbbcbdbe',
+ 'ecb-tbl-256: I=53'),
+ ('2b6ff8d7a5cc3a28a22d5a6f221af26b', '69a6eab00432517d0bf483c91c0963c7',
+ 'c0c1c2c3c5c6c7c8cacbcccdcfd0d1d2d4d5d6d7d9dadbdcdedfe0e1e3e4e5e6',
+ 'ecb-tbl-256: I=54'),
+ ('1a9527c29b8add4b0e3e656dbb2af8b4', '0797f41dc217c80446e1d514bd6ab197',
+ 'e8e9eaebedeeeff0f2f3f4f5f7f8f9fafcfdfeff01020304060708090b0c0d0e',
+ 'ecb-tbl-256: I=55'),
+ ('7f99cf2c75244df015eb4b0c1050aeae', '9dfd76575902a637c01343c58e011a03',
+ '10111213151617181a1b1c1d1f20212224252627292a2b2c2e2f303133343536',
+ 'ecb-tbl-256: I=56'),
+ ('e84ff85b0d9454071909c1381646c4ed', 'acf4328ae78f34b9fa9b459747cc2658',
+ '38393a3b3d3e3f40424344454748494a4c4d4e4f51525354565758595b5c5d5e',
+ 'ecb-tbl-256: I=57'),
+ ('89afd40f99521280d5399b12404f6db4', 'b0479aea12bac4fe2384cf98995150c6',
+ '60616263656667686a6b6c6d6f70717274757677797a7b7c7e7f808183848586',
+ 'ecb-tbl-256: I=58'),
+ ('a09ef32dbc5119a35ab7fa38656f0329', '9dd52789efe3ffb99f33b3da5030109a',
+ '88898a8b8d8e8f90929394959798999a9c9d9e9fa1a2a3a4a6a7a8a9abacadae',
+ 'ecb-tbl-256: I=59'),
+ ('61773457f068c376c7829b93e696e716', 'abbb755e4621ef8f1214c19f649fb9fd',
+ 'b0b1b2b3b5b6b7b8babbbcbdbfc0c1c2c4c5c6c7c9cacbcccecfd0d1d3d4d5d6',
+ 'ecb-tbl-256: I=60'),
+ ('a34f0cae726cce41dd498747d891b967', 'da27fb8174357bce2bed0e7354f380f9',
+ 'd8d9dadbdddedfe0e2e3e4e5e7e8e9eaecedeeeff1f2f3f4f6f7f8f9fbfcfdfe',
+ 'ecb-tbl-256: I=61'),
+ ('856f59496c7388ee2d2b1a27b7697847', 'c59a0663f0993838f6e5856593bdc5ef',
+ '00010203050607080a0b0c0d0f10111214151617191a1b1c1e1f202123242526',
+ 'ecb-tbl-256: I=62'),
+ ('cb090c593ef7720bd95908fb93b49df4', 'ed60b264b5213e831607a99c0ce5e57e',
+ '28292a2b2d2e2f30323334353738393a3c3d3e3f41424344464748494b4c4d4e',
+ 'ecb-tbl-256: I=63'),
+ ('a0ac75cd2f1923d460fc4d457ad95baf', 'e50548746846f3eb77b8c520640884ed',
+ '50515253555657585a5b5c5d5f60616264656667696a6b6c6e6f707173747576',
+ 'ecb-tbl-256: I=64'),
+ ('2a2b282974777689e8e9eeef525d5c5f', '28282cc7d21d6a2923641e52d188ef0c',
+ '78797a7b7d7e7f80828384858788898a8c8d8e8f91929394969798999b9c9d9e',
+ 'ecb-tbl-256: I=65'),
+ ('909192939390919e0f0e09089788898a', '0dfa5b02abb18e5a815305216d6d4f8e',
+ 'a0a1a2a3a5a6a7a8aaabacadafb0b1b2b4b5b6b7b9babbbcbebfc0c1c3c4c5c6',
+ 'ecb-tbl-256: I=66'),
+ ('777675748d8e8f907170777649464744', '7359635c0eecefe31d673395fb46fb99',
+ 'c8c9cacbcdcecfd0d2d3d4d5d7d8d9dadcdddedfe1e2e3e4e6e7e8e9ebecedee',
+ 'ecb-tbl-256: I=67'),
+ ('717073720605040b2d2c2b2a05fafbf9', '73c679f7d5aef2745c9737bb4c47fb36',
+ 'f0f1f2f3f5f6f7f8fafbfcfdfe01000204050607090a0b0c0e0f101113141516',
+ 'ecb-tbl-256: I=68'),
+ ('64656667fefdfcc31b1a1d1ca5aaaba8', 'b192bd472a4d2eafb786e97458967626',
+ '18191a1b1d1e1f20222324252728292a2c2d2e2f31323334363738393b3c3d3e',
+ 'ecb-tbl-256: I=69'),
+ ('dbdad9d86a696867b5b4b3b2c8d7d6d5', '0ec327f6c8a2b147598ca3fde61dc6a4',
+ '40414243454647484a4b4c4d4f50515254555657595a5b5c5e5f606163646566',
+ 'ecb-tbl-256: I=70'),
+ ('5c5d5e5fe3e0e1fe31303736333c3d3e', 'fc418eb3c41b859b38d4b6f646629729',
+ '68696a6b6d6e6f70727374757778797a7c7d7e7f81828384868788898b8c8d8e',
+ 'ecb-tbl-256: I=71'),
+ ('545556574b48494673727574546b6a69', '30249e5ac282b1c981ea64b609f3a154',
+ '90919293959697989a9b9c9d9fa0a1a2a4a5a6a7a9aaabacaeafb0b1b3b4b5b6',
+ 'ecb-tbl-256: I=72'),
+ ('ecedeeefc6c5c4bb56575051f5fafbf8', '5e6e08646d12150776bb43c2d78a9703',
+ 'b8b9babbbdbebfc0c2c3c4c5c7c8c9cacccdcecfd1d2d3d4d6d7d8d9dbdcddde',
+ 'ecb-tbl-256: I=73'),
+ ('464744452724252ac9c8cfced2cdcccf', 'faeb3d5de652cd3447dceb343f30394a',
+ 'e0e1e2e3e5e6e7e8eaebecedeff0f1f2f4f5f6f7f9fafbfcfefe010103040506',
+ 'ecb-tbl-256: I=74'),
+ ('e6e7e4e54142435c878681801c131211', 'a8e88706823f6993ef80d05c1c7b2cf0',
+ '08090a0b0d0e0f10121314151718191a1c1d1e1f21222324262728292b2c2d2e',
+ 'ecb-tbl-256: I=75'),
+ ('72737071cfcccdc2f9f8fffe710e0f0c', '8ced86677e6e00a1a1b15968f2d3cce6',
+ '30313233353637383a3b3c3d3f40414244454647494a4b4c4e4f505153545556',
+ 'ecb-tbl-256: I=76'),
+ ('505152537370714ec3c2c5c4010e0f0c', '9fc7c23858be03bdebb84e90db6786a9',
+ '58595a5b5d5e5f60626364656768696a6c6d6e6f71727374767778797b7c7d7e',
+ 'ecb-tbl-256: I=77'),
+ ('a8a9aaab5c5f5e51aeafa8a93d222320', 'b4fbd65b33f70d8cf7f1111ac4649c36',
+ '80818283858687888a8b8c8d8f90919294959697999a9b9c9e9fa0a1a3a4a5a6',
+ 'ecb-tbl-256: I=78'),
+ ('dedfdcddf6f5f4eb10111617fef1f0f3', 'c5c32d5ed03c4b53cc8c1bd0ef0dbbf6',
+ 'a8a9aaabadaeafb0b2b3b4b5b7b8b9babcbdbebfc1c2c3c4c6c7c8c9cbcccdce',
+ 'ecb-tbl-256: I=79'),
+ ('bdbcbfbe5e5d5c530b0a0d0cfac5c4c7', 'd1a7f03b773e5c212464b63709c6a891',
+ 'd0d1d2d3d5d6d7d8dadbdcdddfe0e1e2e4e5e6e7e9eaebeceeeff0f1f3f4f5f6',
+ 'ecb-tbl-256: I=80'),
+ ('8a8b8889050606f8f4f5f2f3636c6d6e', '6b7161d8745947ac6950438ea138d028',
+ 'f8f9fafbfdfefe00020304050708090a0c0d0e0f11121314161718191b1c1d1e',
+ 'ecb-tbl-256: I=81'),
+ ('a6a7a4a54d4e4f40b2b3b4b539262724', 'fd47a9f7e366ee7a09bc508b00460661',
+ '20212223252627282a2b2c2d2f30313234353637393a3b3c3e3f404143444546',
+ 'ecb-tbl-256: I=82'),
+ ('9c9d9e9fe9eaebf40e0f08099b949596', '00d40b003dc3a0d9310b659b98c7e416',
+ '48494a4b4d4e4f50525354555758595a5c5d5e5f61626364666768696b6c6d6e',
+ 'ecb-tbl-256: I=83'),
+ ('2d2c2f2e1013121dcccdcacbed121310', 'eea4c79dcc8e2bda691f20ac48be0717',
+ '70717273757677787a7b7c7d7f80818284858687898a8b8c8e8f909193949596',
+ 'ecb-tbl-256: I=84'),
+ ('f4f5f6f7edeeefd0eaebecedf7f8f9fa', 'e78f43b11c204403e5751f89d05a2509',
+ '98999a9b9d9e9fa0a2a3a4a5a7a8a9aaacadaeafb1b2b3b4b6b7b8b9bbbcbdbe',
+ 'ecb-tbl-256: I=85'),
+ ('3d3c3f3e282b2a2573727574150a0b08', 'd0f0e3d1f1244bb979931e38dd1786ef',
+ 'c0c1c2c3c5c6c7c8cacbcccdcfd0d1d2d4d5d6d7d9dadbdcdedfe0e1e3e4e5e6',
+ 'ecb-tbl-256: I=86'),
+ ('b6b7b4b5f8fbfae5b4b5b2b3a0afaead', '042e639dc4e1e4dde7b75b749ea6f765',
+ 'e8e9eaebedeeeff0f2f3f4f5f7f8f9fafcfdfeff01020304060708090b0c0d0e',
+ 'ecb-tbl-256: I=87'),
+ ('b7b6b5b4989b9a95878681809ba4a5a6', 'bc032fdd0efe29503a980a7d07ab46a8',
+ '10111213151617181a1b1c1d1f20212224252627292a2b2c2e2f303133343536',
+ 'ecb-tbl-256: I=88'),
+ ('a8a9aaabe5e6e798e9e8efee4748494a', '0c93ac949c0da6446effb86183b6c910',
+ '38393a3b3d3e3f40424344454748494a4c4d4e4f51525354565758595b5c5d5e',
+ 'ecb-tbl-256: I=89'),
+ ('ecedeeefd9dadbd4b9b8bfbe657a7b78', 'e0d343e14da75c917b4a5cec4810d7c2',
+ '60616263656667686a6b6c6d6f70717274757677797a7b7c7e7f808183848586',
+ 'ecb-tbl-256: I=90'),
+ ('7f7e7d7c696a6b74cacbcccd929d9c9f', '0eafb821748408279b937b626792e619',
+ '88898a8b8d8e8f90929394959798999a9c9d9e9fa1a2a3a4a6a7a8a9abacadae',
+ 'ecb-tbl-256: I=91'),
+ ('08090a0b0605040bfffef9f8b9c6c7c4', 'fa1ac6e02d23b106a1fef18b274a553f',
+ 'b0b1b2b3b5b6b7b8babbbcbdbfc0c1c2c4c5c6c7c9cacbcccecfd0d1d3d4d5d6',
+ 'ecb-tbl-256: I=92'),
+ ('08090a0bf1f2f3ccfcfdfafb68676665', '0dadfe019cd12368075507df33c1a1e9',
+ 'd8d9dadbdddedfe0e2e3e4e5e7e8e9eaecedeeeff1f2f3f4f6f7f8f9fbfcfdfe',
+ 'ecb-tbl-256: I=93'),
+ ('cacbc8c93a393837050403020d121310', '3a0879b414465d9ffbaf86b33a63a1b9',
+ '00010203050607080a0b0c0d0f10111214151617191a1b1c1e1f202123242526',
+ 'ecb-tbl-256: I=94'),
+ ('e9e8ebea8281809f8f8e8988343b3a39', '62199fadc76d0be1805d3ba0b7d914bf',
+ '28292a2b2d2e2f30323334353738393a3c3d3e3f41424344464748494b4c4d4e',
+ 'ecb-tbl-256: I=95'),
+ ('515053524645444bd0d1d6d7340b0a09', '1b06d6c5d333e742730130cf78e719b4',
+ '50515253555657585a5b5c5d5f60616264656667696a6b6c6e6f707173747576',
+ 'ecb-tbl-256: I=96'),
+ ('42434041ecefee1193929594c6c9c8cb', 'f1f848824c32e9dcdcbf21580f069329',
+ '78797a7b7d7e7f80828384858788898a8c8d8e8f91929394969798999b9c9d9e',
+ 'ecb-tbl-256: I=97'),
+ ('efeeedecc2c1c0cf76777071455a5b58', '1a09050cbd684f784d8e965e0782f28a',
+ 'a0a1a2a3a5a6a7a8aaabacadafb0b1b2b4b5b6b7b9babbbcbebfc0c1c3c4c5c6',
+ 'ecb-tbl-256: I=98'),
+ ('5f5e5d5c3f3c3d221d1c1b1a19161714', '79c2969e7ded2ba7d088f3f320692360',
+ 'c8c9cacbcdcecfd0d2d3d4d5d7d8d9dadcdddedfe1e2e3e4e6e7e8e9ebecedee',
+ 'ecb-tbl-256: I=99'),
+ ('000102034142434c1c1d1a1b8d727371', '091a658a2f7444c16accb669450c7b63',
+ 'f0f1f2f3f5f6f7f8fafbfcfdfe01000204050607090a0b0c0e0f101113141516',
+ 'ecb-tbl-256: I=100'),
+ ('8e8f8c8db1b2b38c56575051050a0b08', '97c1e3a72cca65fa977d5ed0e8a7bbfc',
+ '18191a1b1d1e1f20222324252728292a2c2d2e2f31323334363738393b3c3d3e',
+ 'ecb-tbl-256: I=101'),
+ ('a7a6a5a4e8ebeae57f7e7978cad5d4d7', '70c430c6db9a17828937305a2df91a2a',
+ '40414243454647484a4b4c4d4f50515254555657595a5b5c5e5f606163646566',
+ 'ecb-tbl-256: I=102'),
+ ('8a8b888994979689454443429f909192', '629553457fbe2479098571c7c903fde8',
+ '68696a6b6d6e6f70727374757778797a7c7d7e7f81828384868788898b8c8d8e',
+ 'ecb-tbl-256: I=103'),
+ ('8c8d8e8fe0e3e2ed45444342f1cecfcc', 'a25b25a61f612669e7d91265c7d476ba',
+ '90919293959697989a9b9c9d9fa0a1a2a4a5a6a7a9aaabacaeafb0b1b3b4b5b6',
+ 'ecb-tbl-256: I=104'),
+ ('fffefdfc4c4f4e31d8d9dedfb6b9b8bb', 'eb7e4e49b8ae0f024570dda293254fed',
+ 'b8b9babbbdbebfc0c2c3c4c5c7c8c9cacccdcecfd1d2d3d4d6d7d8d9dbdcddde',
+ 'ecb-tbl-256: I=105'),
+ ('fdfcfffecccfcec12f2e29286679787b', '38fe15d61cca84516e924adce5014f67',
+ 'e0e1e2e3e5e6e7e8eaebecedeff0f1f2f4f5f6f7f9fafbfcfefe010103040506',
+ 'ecb-tbl-256: I=106'),
+ ('67666564bab9b8a77071767719161714', '3ad208492249108c9f3ebeb167ad0583',
+ '08090a0b0d0e0f10121314151718191a1c1d1e1f21222324262728292b2c2d2e',
+ 'ecb-tbl-256: I=107'),
+ ('9a9b98992d2e2f2084858283245b5a59', '299ba9f9bf5ab05c3580fc26edd1ed12',
+ '30313233353637383a3b3c3d3f40414244454647494a4b4c4e4f505153545556',
+ 'ecb-tbl-256: I=108'),
+ ('a4a5a6a70b0809365c5d5a5b2c232221', '19dc705b857a60fb07717b2ea5717781',
+ '58595a5b5d5e5f60626364656768696a6c6d6e6f71727374767778797b7c7d7e',
+ 'ecb-tbl-256: I=109'),
+ ('464744455754555af3f2f5f4afb0b1b2', 'ffc8aeb885b5efcad06b6dbebf92e76b',
+ '80818283858687888a8b8c8d8f90919294959697999a9b9c9e9fa0a1a3a4a5a6',
+ 'ecb-tbl-256: I=110'),
+ ('323330317675746b7273747549464744', 'f58900c5e0b385253ff2546250a0142b',
+ 'a8a9aaabadaeafb0b2b3b4b5b7b8b9babcbdbebfc1c2c3c4c6c7c8c9cbcccdce',
+ 'ecb-tbl-256: I=111'),
+ ('a8a9aaab181b1a15808186872b141516', '2ee67b56280bc462429cee6e3370cbc1',
+ 'd0d1d2d3d5d6d7d8dadbdcdddfe0e1e2e4e5e6e7e9eaebeceeeff0f1f3f4f5f6',
+ 'ecb-tbl-256: I=112'),
+ ('e7e6e5e4202323ddaaabacad343b3a39', '20db650a9c8e9a84ab4d25f7edc8f03f',
+ 'f8f9fafbfdfefe00020304050708090a0c0d0e0f11121314161718191b1c1d1e',
+ 'ecb-tbl-256: I=113'),
+ ('a8a9aaab2221202fedecebea1e010003', '3c36da169525cf818843805f25b78ae5',
+ '20212223252627282a2b2c2d2f30313234353637393a3b3c3e3f404143444546',
+ 'ecb-tbl-256: I=114'),
+ ('f9f8fbfa5f5c5d42424344450e010003', '9a781d960db9e45e37779042fea51922',
+ '48494a4b4d4e4f50525354555758595a5c5d5e5f61626364666768696b6c6d6e',
+ 'ecb-tbl-256: I=115'),
+ ('57565554f5f6f7f89697909120dfdedd', '6560395ec269c672a3c288226efdba77',
+ '70717273757677787a7b7c7d7f80818284858687898a8b8c8e8f909193949596',
+ 'ecb-tbl-256: I=116'),
+ ('f8f9fafbcccfcef1dddcdbda0e010003', '8c772b7a189ac544453d5916ebb27b9a',
+ '98999a9b9d9e9fa0a2a3a4a5a7a8a9aaacadaeafb1b2b3b4b6b7b8b9bbbcbdbe',
+ 'ecb-tbl-256: I=117'),
+ ('d9d8dbda7073727d80818687c2dddcdf', '77ca5468cc48e843d05f78eed9d6578f',
+ 'c0c1c2c3c5c6c7c8cacbcccdcfd0d1d2d4d5d6d7d9dadbdcdedfe0e1e3e4e5e6',
+ 'ecb-tbl-256: I=118'),
+ ('c5c4c7c6080b0a1588898e8f68676665', '72cdcc71dc82c60d4429c9e2d8195baa',
+ 'e8e9eaebedeeeff0f2f3f4f5f7f8f9fafcfdfeff01020304060708090b0c0d0e',
+ 'ecb-tbl-256: I=119'),
+ ('83828180dcdfded186878081f0cfcecd', '8080d68ce60e94b40b5b8b69eeb35afa',
+ '10111213151617181a1b1c1d1f20212224252627292a2b2c2e2f303133343536',
+ 'ecb-tbl-256: I=120'),
+ ('98999a9bdddedfa079787f7e0a050407', '44222d3cde299c04369d58ac0eba1e8e',
+ '38393a3b3d3e3f40424344454748494a4c4d4e4f51525354565758595b5c5d5e',
+ 'ecb-tbl-256: I=121'),
+ ('cecfcccd4f4c4d429f9e9998dfc0c1c2', '9b8721b0a8dfc691c5bc5885dbfcb27a',
+ '60616263656667686a6b6c6d6f70717274757677797a7b7c7e7f808183848586',
+ 'ecb-tbl-256: I=122'),
+ ('404142436665647b29282f2eaba4a5a6', '0dc015ce9a3a3414b5e62ec643384183',
+ '88898a8b8d8e8f90929394959798999a9c9d9e9fa1a2a3a4a6a7a8a9abacadae',
+ 'ecb-tbl-256: I=123'),
+ ('33323130e6e5e4eb23222524dea1a0a3', '705715448a8da412025ce38345c2a148',
+ 'b0b1b2b3b5b6b7b8babbbcbdbfc0c1c2c4c5c6c7c9cacbcccecfd0d1d3d4d5d6',
+ 'ecb-tbl-256: I=124'),
+ ('cfcecdccf6f5f4cbe6e7e0e199969794', 'c32b5b0b6fbae165266c569f4b6ecf0b',
+ 'd8d9dadbdddedfe0e2e3e4e5e7e8e9eaecedeeeff1f2f3f4f6f7f8f9fbfcfdfe',
+ 'ecb-tbl-256: I=125'),
+ ('babbb8b97271707fdcdddadb29363734', '4dca6c75192a01ddca9476af2a521e87',
+ '00010203050607080a0b0c0d0f10111214151617191a1b1c1e1f202123242526',
+ 'ecb-tbl-256: I=126'),
+ ('c9c8cbca4447465926272021545b5a59', '058691e627ecbc36ac07b6db423bd698',
+ '28292a2b2d2e2f30323334353738393a3c3d3e3f41424344464748494b4c4d4e',
+ 'ecb-tbl-256: I=127'),
+ ('050407067477767956575051221d1c1f', '7444527095838fe080fc2bcdd30847eb',
+ '50515253555657585a5b5c5d5f60616264656667696a6b6c6e6f707173747576',
+ 'ecb-tbl-256: I=128'),
+
+ # FIPS PUB 800-38A test vectors, 2001 edition. Annex F.
+
+ ('6bc1bee22e409f96e93d7e117393172a'+'ae2d8a571e03ac9c9eb76fac45af8e51'+
+ '30c81c46a35ce411e5fbc1191a0a52ef'+'f69f2445df4f9b17ad2b417be66c3710',
+ '3ad77bb40d7a3660a89ecaf32466ef97'+'f5d3d58503b9699de785895a96fdbaaf'+
+ '43b1cd7f598ece23881b00e3ed030688'+'7b0c785e27e8ad3f8223207104725dd4',
+ '2b7e151628aed2a6abf7158809cf4f3c',
+ 'NIST 800-38A, F.1.1, ECB and AES-128'),
+
+ ('6bc1bee22e409f96e93d7e117393172a'+'ae2d8a571e03ac9c9eb76fac45af8e51'+
+ '30c81c46a35ce411e5fbc1191a0a52ef'+'f69f2445df4f9b17ad2b417be66c3710',
+ 'bd334f1d6e45f25ff712a214571fa5cc'+'974104846d0ad3ad7734ecb3ecee4eef'+
+ 'ef7afd2270e2e60adce0ba2face6444e'+'9a4b41ba738d6c72fb16691603c18e0e',
+ '8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b',
+ 'NIST 800-38A, F.1.3, ECB and AES-192'),
+
+ ('6bc1bee22e409f96e93d7e117393172a'+'ae2d8a571e03ac9c9eb76fac45af8e51'+
+ '30c81c46a35ce411e5fbc1191a0a52ef'+'f69f2445df4f9b17ad2b417be66c3710',
+ 'f3eed1bdb5d2a03c064b5a7e3db181f8'+'591ccb10d410ed26dc5ba74a31362870'+
+ 'b6ed21b99ca6f4f9f153e7b1beafed1d'+'23304b7a39f9f3ff067d8d8f9e24ecc7',
+ '603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4',
+ 'NIST 800-38A, F.1.3, ECB and AES-256'),
+
+]
+
+test_data_8_lanes = []
+for td in test_data:
+ test_data_8_lanes.append((td[0] * 8, td[1] * 8, td[2], td[3]))
+test_data += test_data_8_lanes
+
+class TestMultipleBlocks(unittest.TestCase):
+
+ def __init__(self, use_aesni):
+ unittest.TestCase.__init__(self)
+ self.use_aesni = use_aesni
+
+ def runTest(self):
+ # Encrypt data which is 8*2+4 bytes long, so as to trigger (for the
+ # AESNI variant) both the path that parallelizes 8 lanes and the one
+ # that processes data serially
+
+ tvs = [
+ (b'a' * 16, 'c0b27011eb15bf144d2fc9fae80ea16d4c231cb230416c5fac02e6835ad9d7d0'),
+ (b'a' * 24, 'df8435ce361a78c535b41dcb57da952abbf9ee5954dc6fbcd75fd00fa626915d'),
+ (b'a' * 32, '211402de6c80db1f92ba255881178e1f70783b8cfd3b37808205e48b80486cd8')
+ ]
+
+ for key, expected in tvs:
+
+ cipher = AES.new(key, AES.MODE_ECB, use_aesni=self.use_aesni)
+ h = SHA256.new()
+
+ pt = b"".join([ tobytes('{0:016x}'.format(x)) for x in range(20) ])
+ ct = cipher.encrypt(pt)
+ self.assertEqual(SHA256.new(ct).hexdigest(), expected)
+
+
+class TestIncompleteBlocks(unittest.TestCase):
+
+ def __init__(self, use_aesni):
+ unittest.TestCase.__init__(self)
+ self.use_aesni = use_aesni
+
+ def runTest(self):
+ # Encrypt data with length not multiple of 16 bytes
+
+ cipher = AES.new(b'4'*16, AES.MODE_ECB, use_aesni=self.use_aesni)
+
+ for msg_len in range(1, 16):
+ self.assertRaises(ValueError, cipher.encrypt, b'1' * msg_len)
+ self.assertRaises(ValueError, cipher.encrypt, b'1' * (msg_len+16))
+ self.assertRaises(ValueError, cipher.decrypt, b'1' * msg_len)
+ self.assertRaises(ValueError, cipher.decrypt, b'1' * (msg_len+16))
+
+ self.assertEqual(cipher.encrypt(b''), b'')
+ self.assertEqual(cipher.decrypt(b''), b'')
+
+
+class TestOutput(unittest.TestCase):
+
+ def __init__(self, use_aesni):
+ unittest.TestCase.__init__(self)
+ self.use_aesni = use_aesni
+
+ def runTest(self):
+ # Encrypt/Decrypt data and test output parameter
+
+ cipher = AES.new(b'4'*16, AES.MODE_ECB, use_aesni=self.use_aesni)
+
+ pt = b'5' * 16
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(16)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ output = memoryview(bytearray(16))
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0'*16)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0'*16)
+
+ shorter_output = bytearray(15)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+def get_tests(config={}):
+ from Crypto.Util import _cpu_features
+ from .common import make_block_tests
+
+ tests = make_block_tests(AES, "AES", test_data, {'use_aesni': False})
+ tests += [ TestMultipleBlocks(False) ]
+ tests += [ TestIncompleteBlocks(False) ]
+ if _cpu_features.have_aes_ni():
+ # Run tests with AES-NI instructions if they are available.
+ tests += make_block_tests(AES, "AESNI", test_data, {'use_aesni': True})
+ tests += [ TestMultipleBlocks(True) ]
+ tests += [ TestIncompleteBlocks(True) ]
+ tests += [ TestOutput(True) ]
+ else:
+ print("Skipping AESNI tests")
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ARC2.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ARC2.py
new file mode 100644
index 00000000..fd9448c1
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ARC2.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/ARC2.py: Self-test for the Alleged-RC2 cipher
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Cipher.ARC2"""
+
+import unittest
+
+from Crypto.Util.py3compat import b, bchr
+
+from Crypto.Cipher import ARC2
+
+# This is a list of (plaintext, ciphertext, key[, description[, extra_params]]) tuples.
+test_data = [
+ # Test vectors from RFC 2268
+
+ # 63-bit effective key length
+ ('0000000000000000', 'ebb773f993278eff', '0000000000000000',
+ 'RFC2268-1', dict(effective_keylen=63)),
+
+ # 64-bit effective key length
+ ('ffffffffffffffff', '278b27e42e2f0d49', 'ffffffffffffffff',
+ 'RFC2268-2', dict(effective_keylen=64)),
+ ('1000000000000001', '30649edf9be7d2c2', '3000000000000000',
+ 'RFC2268-3', dict(effective_keylen=64)),
+ #('0000000000000000', '61a8a244adacccf0', '88',
+ # 'RFC2268-4', dict(effective_keylen=64)),
+ ('0000000000000000', '6ccf4308974c267f', '88bca90e90875a',
+ 'RFC2268-5', dict(effective_keylen=64)),
+ ('0000000000000000', '1a807d272bbe5db1', '88bca90e90875a7f0f79c384627bafb2',
+ 'RFC2268-6', dict(effective_keylen=64)),
+
+ # 128-bit effective key length
+ ('0000000000000000', '2269552ab0f85ca6', '88bca90e90875a7f0f79c384627bafb2',
+ "RFC2268-7", dict(effective_keylen=128)),
+ ('0000000000000000', '5b78d3a43dfff1f1',
+ '88bca90e90875a7f0f79c384627bafb216f80a6f85920584c42fceb0be255daf1e',
+ "RFC2268-8", dict(effective_keylen=129)),
+
+ # Test vectors from PyCrypto 2.0.1's testdata.py
+ # 1024-bit effective key length
+ ('0000000000000000', '624fb3e887419e48', '5068696c6970476c617373',
+ 'PCTv201-0'),
+ ('ffffffffffffffff', '79cadef44c4a5a85', '5068696c6970476c617373',
+ 'PCTv201-1'),
+ ('0001020304050607', '90411525b34e4c2c', '5068696c6970476c617373',
+ 'PCTv201-2'),
+ ('0011223344556677', '078656aaba61cbfb', '5068696c6970476c617373',
+ 'PCTv201-3'),
+ ('0000000000000000', 'd7bcc5dbb4d6e56a', 'ffffffffffffffff',
+ 'PCTv201-4'),
+ ('ffffffffffffffff', '7259018ec557b357', 'ffffffffffffffff',
+ 'PCTv201-5'),
+ ('0001020304050607', '93d20a497f2ccb62', 'ffffffffffffffff',
+ 'PCTv201-6'),
+ ('0011223344556677', 'cb15a7f819c0014d', 'ffffffffffffffff',
+ 'PCTv201-7'),
+ ('0000000000000000', '63ac98cdf3843a7a', 'ffffffffffffffff5065746572477265656e6177617953e5ffe553',
+ 'PCTv201-8'),
+ ('ffffffffffffffff', '3fb49e2fa12371dd', 'ffffffffffffffff5065746572477265656e6177617953e5ffe553',
+ 'PCTv201-9'),
+ ('0001020304050607', '46414781ab387d5f', 'ffffffffffffffff5065746572477265656e6177617953e5ffe553',
+ 'PCTv201-10'),
+ ('0011223344556677', 'be09dc81feaca271', 'ffffffffffffffff5065746572477265656e6177617953e5ffe553',
+ 'PCTv201-11'),
+ ('0000000000000000', 'e64221e608be30ab', '53e5ffe553',
+ 'PCTv201-12'),
+ ('ffffffffffffffff', '862bc60fdcd4d9a9', '53e5ffe553',
+ 'PCTv201-13'),
+ ('0001020304050607', '6a34da50fa5e47de', '53e5ffe553',
+ 'PCTv201-14'),
+ ('0011223344556677', '584644c34503122c', '53e5ffe553',
+ 'PCTv201-15'),
+]
+
+class BufferOverflowTest(unittest.TestCase):
+ # Test a buffer overflow found in older versions of PyCrypto
+
+ def runTest(self):
+ """ARC2 with keylength > 128"""
+ key = b("x") * 16384
+ self.assertRaises(ValueError, ARC2.new, key, ARC2.MODE_ECB)
+
+class KeyLength(unittest.TestCase):
+
+ def runTest(self):
+ ARC2.new(b'\x00' * 16, ARC2.MODE_ECB, effective_keylen=40)
+ self.assertRaises(ValueError, ARC2.new, bchr(0) * 4, ARC2.MODE_ECB)
+ self.assertRaises(ValueError, ARC2.new, bchr(0) * 129, ARC2.MODE_ECB)
+
+ self.assertRaises(ValueError, ARC2.new, bchr(0) * 16, ARC2.MODE_ECB,
+ effective_keylen=39)
+ self.assertRaises(ValueError, ARC2.new, bchr(0) * 16, ARC2.MODE_ECB,
+ effective_keylen=1025)
+
+
+class TestOutput(unittest.TestCase):
+
+ def runTest(self):
+ # Encrypt/Decrypt data and test output parameter
+
+ cipher = ARC2.new(b'4'*16, ARC2.MODE_ECB)
+
+ pt = b'5' * 16
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(16)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ output = memoryview(bytearray(16))
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0'*16)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0'*16)
+
+ shorter_output = bytearray(7)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+def get_tests(config={}):
+ from Crypto.Cipher import ARC2
+ from .common import make_block_tests
+
+ tests = make_block_tests(ARC2, "ARC2", test_data)
+ tests.append(BufferOverflowTest())
+ tests.append(KeyLength())
+ tests += [TestOutput()]
+
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ARC4.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ARC4.py
new file mode 100644
index 00000000..753df4cb
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ARC4.py
@@ -0,0 +1,471 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/ARC4.py: Self-test for the Alleged-RC4 cipher
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Cipher.ARC4"""
+
+import unittest
+
+from Crypto.Util.py3compat import b
+from Crypto.SelfTest.st_common import list_test_cases
+from binascii import unhexlify
+
+from Crypto.Cipher import ARC4
+
+# This is a list of (plaintext, ciphertext, key[, description]) tuples.
+test_data = [
+ # Test vectors from Eric Rescorla's message with the subject
+ # "RC4 compatibility testing", sent to the cipherpunks mailing list on
+ # September 13, 1994.
+ # http://cypherpunks.venona.com/date/1994/09/msg00420.html
+
+ ('0123456789abcdef', '75b7878099e0c596', '0123456789abcdef',
+ 'Test vector 0'),
+
+ ('0000000000000000', '7494c2e7104b0879', '0123456789abcdef',
+ 'Test vector 1'),
+
+ ('0000000000000000', 'de188941a3375d3a', '0000000000000000',
+ 'Test vector 2'),
+
+ ('00000000000000000000', 'd6a141a7ec3c38dfbd61', 'ef012345',
+ 'Test vector 3'),
+
+ ('01' * 512,
+ '7595c3e6114a09780c4ad452338e1ffd9a1be9498f813d76533449b6778dcad8'
+ + 'c78a8d2ba9ac66085d0e53d59c26c2d1c490c1ebbe0ce66d1b6b1b13b6b919b8'
+ + '47c25a91447a95e75e4ef16779cde8bf0a95850e32af9689444fd377108f98fd'
+ + 'cbd4e726567500990bcc7e0ca3c4aaa304a387d20f3b8fbbcd42a1bd311d7a43'
+ + '03dda5ab078896ae80c18b0af66dff319616eb784e495ad2ce90d7f772a81747'
+ + 'b65f62093b1e0db9e5ba532fafec47508323e671327df9444432cb7367cec82f'
+ + '5d44c0d00b67d650a075cd4b70dedd77eb9b10231b6b5b741347396d62897421'
+ + 'd43df9b42e446e358e9c11a9b2184ecbef0cd8e7a877ef968f1390ec9b3d35a5'
+ + '585cb009290e2fcde7b5ec66d9084be44055a619d9dd7fc3166f9487f7cb2729'
+ + '12426445998514c15d53a18c864ce3a2b7555793988126520eacf2e3066e230c'
+ + '91bee4dd5304f5fd0405b35bd99c73135d3d9bc335ee049ef69b3867bf2d7bd1'
+ + 'eaa595d8bfc0066ff8d31509eb0c6caa006c807a623ef84c3d33c195d23ee320'
+ + 'c40de0558157c822d4b8c569d849aed59d4e0fd7f379586b4b7ff684ed6a189f'
+ + '7486d49b9c4bad9ba24b96abf924372c8a8fffb10d55354900a77a3db5f205e1'
+ + 'b99fcd8660863a159ad4abe40fa48934163ddde542a6585540fd683cbfd8c00f'
+ + '12129a284deacc4cdefe58be7137541c047126c8d49e2755ab181ab7e940b0c0',
+ '0123456789abcdef',
+ "Test vector 4"),
+ # shortest key - generated with arc4 package
+ ('7468697320697320616e206578616d706c65',
+ '7260677d38495a09585d69321e17eaf3cdd0',
+ '01'),
+]
+
+
+class RFC6229_Tests(unittest.TestCase):
+ # Test vectors from RFC 6229. Each test vector is a tuple with two items:
+ # the ARC4 key and a dictionary. The dictionary has keystream offsets as keys
+ # and the 16-byte keystream starting at the relevant offset as value.
+ rfc6229_data = [
+ # Page 3
+ (
+ '0102030405',
+ {
+ 0: 'b2 39 63 05 f0 3d c0 27 cc c3 52 4a 0a 11 18 a8',
+ 16: '69 82 94 4f 18 fc 82 d5 89 c4 03 a4 7a 0d 09 19',
+ 240: '28 cb 11 32 c9 6c e2 86 42 1d ca ad b8 b6 9e ae',
+ 256: '1c fc f6 2b 03 ed db 64 1d 77 df cf 7f 8d 8c 93',
+ 496: '42 b7 d0 cd d9 18 a8 a3 3d d5 17 81 c8 1f 40 41',
+ 512: '64 59 84 44 32 a7 da 92 3c fb 3e b4 98 06 61 f6',
+ 752: 'ec 10 32 7b de 2b ee fd 18 f9 27 76 80 45 7e 22',
+ 768: 'eb 62 63 8d 4f 0b a1 fe 9f ca 20 e0 5b f8 ff 2b',
+ 1008: '45 12 90 48 e6 a0 ed 0b 56 b4 90 33 8f 07 8d a5',
+ 1024: '30 ab bc c7 c2 0b 01 60 9f 23 ee 2d 5f 6b b7 df',
+ 1520: '32 94 f7 44 d8 f9 79 05 07 e7 0f 62 e5 bb ce ea',
+ 1536: 'd8 72 9d b4 18 82 25 9b ee 4f 82 53 25 f5 a1 30',
+ 2032: '1e b1 4a 0c 13 b3 bf 47 fa 2a 0b a9 3a d4 5b 8b',
+ 2048: 'cc 58 2f 8b a9 f2 65 e2 b1 be 91 12 e9 75 d2 d7',
+ 3056: 'f2 e3 0f 9b d1 02 ec bf 75 aa ad e9 bc 35 c4 3c',
+ 3072: 'ec 0e 11 c4 79 dc 32 9d c8 da 79 68 fe 96 56 81',
+ 4080: '06 83 26 a2 11 84 16 d2 1f 9d 04 b2 cd 1c a0 50',
+ 4096: 'ff 25 b5 89 95 99 67 07 e5 1f bd f0 8b 34 d8 75'
+ }
+ ),
+ # Page 4
+ (
+ '01020304050607',
+ {
+ 0: '29 3f 02 d4 7f 37 c9 b6 33 f2 af 52 85 fe b4 6b',
+ 16: 'e6 20 f1 39 0d 19 bd 84 e2 e0 fd 75 20 31 af c1',
+ 240: '91 4f 02 53 1c 92 18 81 0d f6 0f 67 e3 38 15 4c',
+ 256: 'd0 fd b5 83 07 3c e8 5a b8 39 17 74 0e c0 11 d5',
+ 496: '75 f8 14 11 e8 71 cf fa 70 b9 0c 74 c5 92 e4 54',
+ 512: '0b b8 72 02 93 8d ad 60 9e 87 a5 a1 b0 79 e5 e4',
+ 752: 'c2 91 12 46 b6 12 e7 e7 b9 03 df ed a1 da d8 66',
+ 768: '32 82 8f 91 50 2b 62 91 36 8d e8 08 1d e3 6f c2',
+ 1008: 'f3 b9 a7 e3 b2 97 bf 9a d8 04 51 2f 90 63 ef f1',
+ 1024: '8e cb 67 a9 ba 1f 55 a5 a0 67 e2 b0 26 a3 67 6f',
+ 1520: 'd2 aa 90 2b d4 2d 0d 7c fd 34 0c d4 58 10 52 9f',
+ 1536: '78 b2 72 c9 6e 42 ea b4 c6 0b d9 14 e3 9d 06 e3',
+ 2032: 'f4 33 2f d3 1a 07 93 96 ee 3c ee 3f 2a 4f f0 49',
+ 2048: '05 45 97 81 d4 1f da 7f 30 c1 be 7e 12 46 c6 23',
+ 3056: 'ad fd 38 68 b8 e5 14 85 d5 e6 10 01 7e 3d d6 09',
+ 3072: 'ad 26 58 1c 0c 5b e4 5f 4c ea 01 db 2f 38 05 d5',
+ 4080: 'f3 17 2c ef fc 3b 3d 99 7c 85 cc d5 af 1a 95 0c',
+ 4096: 'e7 4b 0b 97 31 22 7f d3 7c 0e c0 8a 47 dd d8 b8'
+ }
+ ),
+ (
+ '0102030405060708',
+ {
+ 0: '97 ab 8a 1b f0 af b9 61 32 f2 f6 72 58 da 15 a8',
+ 16: '82 63 ef db 45 c4 a1 86 84 ef 87 e6 b1 9e 5b 09',
+ 240: '96 36 eb c9 84 19 26 f4 f7 d1 f3 62 bd df 6e 18',
+ 256: 'd0 a9 90 ff 2c 05 fe f5 b9 03 73 c9 ff 4b 87 0a',
+ 496: '73 23 9f 1d b7 f4 1d 80 b6 43 c0 c5 25 18 ec 63',
+ 512: '16 3b 31 99 23 a6 bd b4 52 7c 62 61 26 70 3c 0f',
+ 752: '49 d6 c8 af 0f 97 14 4a 87 df 21 d9 14 72 f9 66',
+ 768: '44 17 3a 10 3b 66 16 c5 d5 ad 1c ee 40 c8 63 d0',
+ 1008: '27 3c 9c 4b 27 f3 22 e4 e7 16 ef 53 a4 7d e7 a4',
+ 1024: 'c6 d0 e7 b2 26 25 9f a9 02 34 90 b2 61 67 ad 1d',
+ 1520: '1f e8 98 67 13 f0 7c 3d 9a e1 c1 63 ff 8c f9 d3',
+ 1536: '83 69 e1 a9 65 61 0b e8 87 fb d0 c7 91 62 aa fb',
+ 2032: '0a 01 27 ab b4 44 84 b9 fb ef 5a bc ae 1b 57 9f',
+ 2048: 'c2 cd ad c6 40 2e 8e e8 66 e1 f3 7b db 47 e4 2c',
+ 3056: '26 b5 1e a3 7d f8 e1 d6 f7 6f c3 b6 6a 74 29 b3',
+ 3072: 'bc 76 83 20 5d 4f 44 3d c1 f2 9d da 33 15 c8 7b',
+ 4080: 'd5 fa 5a 34 69 d2 9a aa f8 3d 23 58 9d b8 c8 5b',
+ 4096: '3f b4 6e 2c 8f 0f 06 8e dc e8 cd cd 7d fc 58 62'
+ }
+ ),
+ # Page 5
+ (
+ '0102030405060708090a',
+ {
+ 0: 'ed e3 b0 46 43 e5 86 cc 90 7d c2 18 51 70 99 02',
+ 16: '03 51 6b a7 8f 41 3b eb 22 3a a5 d4 d2 df 67 11',
+ 240: '3c fd 6c b5 8e e0 fd de 64 01 76 ad 00 00 04 4d',
+ 256: '48 53 2b 21 fb 60 79 c9 11 4c 0f fd 9c 04 a1 ad',
+ 496: '3e 8c ea 98 01 71 09 97 90 84 b1 ef 92 f9 9d 86',
+ 512: 'e2 0f b4 9b db 33 7e e4 8b 8d 8d c0 f4 af ef fe',
+ 752: '5c 25 21 ea cd 79 66 f1 5e 05 65 44 be a0 d3 15',
+ 768: 'e0 67 a7 03 19 31 a2 46 a6 c3 87 5d 2f 67 8a cb',
+ 1008: 'a6 4f 70 af 88 ae 56 b6 f8 75 81 c0 e2 3e 6b 08',
+ 1024: 'f4 49 03 1d e3 12 81 4e c6 f3 19 29 1f 4a 05 16',
+ 1520: 'bd ae 85 92 4b 3c b1 d0 a2 e3 3a 30 c6 d7 95 99',
+ 1536: '8a 0f ed db ac 86 5a 09 bc d1 27 fb 56 2e d6 0a',
+ 2032: 'b5 5a 0a 5b 51 a1 2a 8b e3 48 99 c3 e0 47 51 1a',
+ 2048: 'd9 a0 9c ea 3c e7 5f e3 96 98 07 03 17 a7 13 39',
+ 3056: '55 22 25 ed 11 77 f4 45 84 ac 8c fa 6c 4e b5 fc',
+ 3072: '7e 82 cb ab fc 95 38 1b 08 09 98 44 21 29 c2 f8',
+ 4080: '1f 13 5e d1 4c e6 0a 91 36 9d 23 22 be f2 5e 3c',
+ 4096: '08 b6 be 45 12 4a 43 e2 eb 77 95 3f 84 dc 85 53'
+ }
+ ),
+ (
+ '0102030405060708090a0b0c0d0e0f10',
+ {
+ 0: '9a c7 cc 9a 60 9d 1e f7 b2 93 28 99 cd e4 1b 97',
+ 16: '52 48 c4 95 90 14 12 6a 6e 8a 84 f1 1d 1a 9e 1c',
+ 240: '06 59 02 e4 b6 20 f6 cc 36 c8 58 9f 66 43 2f 2b',
+ 256: 'd3 9d 56 6b c6 bc e3 01 07 68 15 15 49 f3 87 3f',
+ 496: 'b6 d1 e6 c4 a5 e4 77 1c ad 79 53 8d f2 95 fb 11',
+ 512: 'c6 8c 1d 5c 55 9a 97 41 23 df 1d bc 52 a4 3b 89',
+ 752: 'c5 ec f8 8d e8 97 fd 57 fe d3 01 70 1b 82 a2 59',
+ 768: 'ec cb e1 3d e1 fc c9 1c 11 a0 b2 6c 0b c8 fa 4d',
+ 1008: 'e7 a7 25 74 f8 78 2a e2 6a ab cf 9e bc d6 60 65',
+ 1024: 'bd f0 32 4e 60 83 dc c6 d3 ce dd 3c a8 c5 3c 16',
+ 1520: 'b4 01 10 c4 19 0b 56 22 a9 61 16 b0 01 7e d2 97',
+ 1536: 'ff a0 b5 14 64 7e c0 4f 63 06 b8 92 ae 66 11 81',
+ 2032: 'd0 3d 1b c0 3c d3 3d 70 df f9 fa 5d 71 96 3e bd',
+ 2048: '8a 44 12 64 11 ea a7 8b d5 1e 8d 87 a8 87 9b f5',
+ 3056: 'fa be b7 60 28 ad e2 d0 e4 87 22 e4 6c 46 15 a3',
+ 3072: 'c0 5d 88 ab d5 03 57 f9 35 a6 3c 59 ee 53 76 23',
+ 4080: 'ff 38 26 5c 16 42 c1 ab e8 d3 c2 fe 5e 57 2b f8',
+ 4096: 'a3 6a 4c 30 1a e8 ac 13 61 0c cb c1 22 56 ca cc'
+ }
+ ),
+ # Page 6
+ (
+ '0102030405060708090a0b0c0d0e0f101112131415161718',
+ {
+ 0: '05 95 e5 7f e5 f0 bb 3c 70 6e da c8 a4 b2 db 11',
+ 16: 'df de 31 34 4a 1a f7 69 c7 4f 07 0a ee 9e 23 26',
+ 240: 'b0 6b 9b 1e 19 5d 13 d8 f4 a7 99 5c 45 53 ac 05',
+ 256: '6b d2 37 8e c3 41 c9 a4 2f 37 ba 79 f8 8a 32 ff',
+ 496: 'e7 0b ce 1d f7 64 5a db 5d 2c 41 30 21 5c 35 22',
+ 512: '9a 57 30 c7 fc b4 c9 af 51 ff da 89 c7 f1 ad 22',
+ 752: '04 85 05 5f d4 f6 f0 d9 63 ef 5a b9 a5 47 69 82',
+ 768: '59 1f c6 6b cd a1 0e 45 2b 03 d4 55 1f 6b 62 ac',
+ 1008: '27 53 cc 83 98 8a fa 3e 16 88 a1 d3 b4 2c 9a 02',
+ 1024: '93 61 0d 52 3d 1d 3f 00 62 b3 c2 a3 bb c7 c7 f0',
+ 1520: '96 c2 48 61 0a ad ed fe af 89 78 c0 3d e8 20 5a',
+ 1536: '0e 31 7b 3d 1c 73 b9 e9 a4 68 8f 29 6d 13 3a 19',
+ 2032: 'bd f0 e6 c3 cc a5 b5 b9 d5 33 b6 9c 56 ad a1 20',
+ 2048: '88 a2 18 b6 e2 ec e1 e6 24 6d 44 c7 59 d1 9b 10',
+ 3056: '68 66 39 7e 95 c1 40 53 4f 94 26 34 21 00 6e 40',
+ 3072: '32 cb 0a 1e 95 42 c6 b3 b8 b3 98 ab c3 b0 f1 d5',
+ 4080: '29 a0 b8 ae d5 4a 13 23 24 c6 2e 42 3f 54 b4 c8',
+ 4096: '3c b0 f3 b5 02 0a 98 b8 2a f9 fe 15 44 84 a1 68'
+ }
+ ),
+ (
+ '0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20',
+ {
+ 0: 'ea a6 bd 25 88 0b f9 3d 3f 5d 1e 4c a2 61 1d 91',
+ 16: 'cf a4 5c 9f 7e 71 4b 54 bd fa 80 02 7c b1 43 80',
+ 240: '11 4a e3 44 de d7 1b 35 f2 e6 0f eb ad 72 7f d8',
+ 256: '02 e1 e7 05 6b 0f 62 39 00 49 64 22 94 3e 97 b6',
+ 496: '91 cb 93 c7 87 96 4e 10 d9 52 7d 99 9c 6f 93 6b',
+ 512: '49 b1 8b 42 f8 e8 36 7c be b5 ef 10 4b a1 c7 cd',
+ 752: '87 08 4b 3b a7 00 ba de 95 56 10 67 27 45 b3 74',
+ 768: 'e7 a7 b9 e9 ec 54 0d 5f f4 3b db 12 79 2d 1b 35',
+ 1008: 'c7 99 b5 96 73 8f 6b 01 8c 76 c7 4b 17 59 bd 90',
+ 1024: '7f ec 5b fd 9f 9b 89 ce 65 48 30 90 92 d7 e9 58',
+ 1520: '40 f2 50 b2 6d 1f 09 6a 4a fd 4c 34 0a 58 88 15',
+ 1536: '3e 34 13 5c 79 db 01 02 00 76 76 51 cf 26 30 73',
+ 2032: 'f6 56 ab cc f8 8d d8 27 02 7b 2c e9 17 d4 64 ec',
+ 2048: '18 b6 25 03 bf bc 07 7f ba bb 98 f2 0d 98 ab 34',
+ 3056: '8a ed 95 ee 5b 0d cb fb ef 4e b2 1d 3a 3f 52 f9',
+ 3072: '62 5a 1a b0 0e e3 9a 53 27 34 6b dd b0 1a 9c 18',
+ 4080: 'a1 3a 7c 79 c7 e1 19 b5 ab 02 96 ab 28 c3 00 b9',
+ 4096: 'f3 e4 c0 a2 e0 2d 1d 01 f7 f0 a7 46 18 af 2b 48'
+ }
+ ),
+ # Page 7
+ (
+ '833222772a',
+ {
+ 0: '80 ad 97 bd c9 73 df 8a 2e 87 9e 92 a4 97 ef da',
+ 16: '20 f0 60 c2 f2 e5 12 65 01 d3 d4 fe a1 0d 5f c0',
+ 240: 'fa a1 48 e9 90 46 18 1f ec 6b 20 85 f3 b2 0e d9',
+ 256: 'f0 da f5 ba b3 d5 96 83 98 57 84 6f 73 fb fe 5a',
+ 496: '1c 7e 2f c4 63 92 32 fe 29 75 84 b2 96 99 6b c8',
+ 512: '3d b9 b2 49 40 6c c8 ed ff ac 55 cc d3 22 ba 12',
+ 752: 'e4 f9 f7 e0 06 61 54 bb d1 25 b7 45 56 9b c8 97',
+ 768: '75 d5 ef 26 2b 44 c4 1a 9c f6 3a e1 45 68 e1 b9',
+ 1008: '6d a4 53 db f8 1e 82 33 4a 3d 88 66 cb 50 a1 e3',
+ 1024: '78 28 d0 74 11 9c ab 5c 22 b2 94 d7 a9 bf a0 bb',
+ 1520: 'ad b8 9c ea 9a 15 fb e6 17 29 5b d0 4b 8c a0 5c',
+ 1536: '62 51 d8 7f d4 aa ae 9a 7e 4a d5 c2 17 d3 f3 00',
+ 2032: 'e7 11 9b d6 dd 9b 22 af e8 f8 95 85 43 28 81 e2',
+ 2048: '78 5b 60 fd 7e c4 e9 fc b6 54 5f 35 0d 66 0f ab',
+ 3056: 'af ec c0 37 fd b7 b0 83 8e b3 d7 0b cd 26 83 82',
+ 3072: 'db c1 a7 b4 9d 57 35 8c c9 fa 6d 61 d7 3b 7c f0',
+ 4080: '63 49 d1 26 a3 7a fc ba 89 79 4f 98 04 91 4f dc',
+ 4096: 'bf 42 c3 01 8c 2f 7c 66 bf de 52 49 75 76 81 15'
+ }
+ ),
+ (
+ '1910833222772a',
+ {
+ 0: 'bc 92 22 db d3 27 4d 8f c6 6d 14 cc bd a6 69 0b',
+ 16: '7a e6 27 41 0c 9a 2b e6 93 df 5b b7 48 5a 63 e3',
+ 240: '3f 09 31 aa 03 de fb 30 0f 06 01 03 82 6f 2a 64',
+ 256: 'be aa 9e c8 d5 9b b6 81 29 f3 02 7c 96 36 11 81',
+ 496: '74 e0 4d b4 6d 28 64 8d 7d ee 8a 00 64 b0 6c fe',
+ 512: '9b 5e 81 c6 2f e0 23 c5 5b e4 2f 87 bb f9 32 b8',
+ 752: 'ce 17 8f c1 82 6e fe cb c1 82 f5 79 99 a4 61 40',
+ 768: '8b df 55 cd 55 06 1c 06 db a6 be 11 de 4a 57 8a',
+ 1008: '62 6f 5f 4d ce 65 25 01 f3 08 7d 39 c9 2c c3 49',
+ 1024: '42 da ac 6a 8f 9a b9 a7 fd 13 7c 60 37 82 56 82',
+ 1520: 'cc 03 fd b7 91 92 a2 07 31 2f 53 f5 d4 dc 33 d9',
+ 1536: 'f7 0f 14 12 2a 1c 98 a3 15 5d 28 b8 a0 a8 a4 1d',
+ 2032: '2a 3a 30 7a b2 70 8a 9c 00 fe 0b 42 f9 c2 d6 a1',
+ 2048: '86 26 17 62 7d 22 61 ea b0 b1 24 65 97 ca 0a e9',
+ 3056: '55 f8 77 ce 4f 2e 1d db bf 8e 13 e2 cd e0 fd c8',
+ 3072: '1b 15 56 cb 93 5f 17 33 37 70 5f bb 5d 50 1f c1',
+ 4080: 'ec d0 e9 66 02 be 7f 8d 50 92 81 6c cc f2 c2 e9',
+ 4096: '02 78 81 fa b4 99 3a 1c 26 20 24 a9 4f ff 3f 61'
+ }
+ ),
+ # Page 8
+ (
+ '641910833222772a',
+ {
+ 0: 'bb f6 09 de 94 13 17 2d 07 66 0c b6 80 71 69 26',
+ 16: '46 10 1a 6d ab 43 11 5d 6c 52 2b 4f e9 36 04 a9',
+ 240: 'cb e1 ff f2 1c 96 f3 ee f6 1e 8f e0 54 2c bd f0',
+ 256: '34 79 38 bf fa 40 09 c5 12 cf b4 03 4b 0d d1 a7',
+ 496: '78 67 a7 86 d0 0a 71 47 90 4d 76 dd f1 e5 20 e3',
+ 512: '8d 3e 9e 1c ae fc cc b3 fb f8 d1 8f 64 12 0b 32',
+ 752: '94 23 37 f8 fd 76 f0 fa e8 c5 2d 79 54 81 06 72',
+ 768: 'b8 54 8c 10 f5 16 67 f6 e6 0e 18 2f a1 9b 30 f7',
+ 1008: '02 11 c7 c6 19 0c 9e fd 12 37 c3 4c 8f 2e 06 c4',
+ 1024: 'bd a6 4f 65 27 6d 2a ac b8 f9 02 12 20 3a 80 8e',
+ 1520: 'bd 38 20 f7 32 ff b5 3e c1 93 e7 9d 33 e2 7c 73',
+ 1536: 'd0 16 86 16 86 19 07 d4 82 e3 6c da c8 cf 57 49',
+ 2032: '97 b0 f0 f2 24 b2 d2 31 71 14 80 8f b0 3a f7 a0',
+ 2048: 'e5 96 16 e4 69 78 79 39 a0 63 ce ea 9a f9 56 d1',
+ 3056: 'c4 7e 0d c1 66 09 19 c1 11 01 20 8f 9e 69 aa 1f',
+ 3072: '5a e4 f1 28 96 b8 37 9a 2a ad 89 b5 b5 53 d6 b0',
+ 4080: '6b 6b 09 8d 0c 29 3b c2 99 3d 80 bf 05 18 b6 d9',
+ 4096: '81 70 cc 3c cd 92 a6 98 62 1b 93 9d d3 8f e7 b9'
+ }
+ ),
+ (
+ '8b37641910833222772a',
+ {
+ 0: 'ab 65 c2 6e dd b2 87 60 0d b2 fd a1 0d 1e 60 5c',
+ 16: 'bb 75 90 10 c2 96 58 f2 c7 2d 93 a2 d1 6d 29 30',
+ 240: 'b9 01 e8 03 6e d1 c3 83 cd 3c 4c 4d d0 a6 ab 05',
+ 256: '3d 25 ce 49 22 92 4c 55 f0 64 94 33 53 d7 8a 6c',
+ 496: '12 c1 aa 44 bb f8 7e 75 e6 11 f6 9b 2c 38 f4 9b',
+ 512: '28 f2 b3 43 4b 65 c0 98 77 47 00 44 c6 ea 17 0d',
+ 752: 'bd 9e f8 22 de 52 88 19 61 34 cf 8a f7 83 93 04',
+ 768: '67 55 9c 23 f0 52 15 84 70 a2 96 f7 25 73 5a 32',
+ 1008: '8b ab 26 fb c2 c1 2b 0f 13 e2 ab 18 5e ab f2 41',
+ 1024: '31 18 5a 6d 69 6f 0c fa 9b 42 80 8b 38 e1 32 a2',
+ 1520: '56 4d 3d ae 18 3c 52 34 c8 af 1e 51 06 1c 44 b5',
+ 1536: '3c 07 78 a7 b5 f7 2d 3c 23 a3 13 5c 7d 67 b9 f4',
+ 2032: 'f3 43 69 89 0f cf 16 fb 51 7d ca ae 44 63 b2 dd',
+ 2048: '02 f3 1c 81 e8 20 07 31 b8 99 b0 28 e7 91 bf a7',
+ 3056: '72 da 64 62 83 22 8c 14 30 08 53 70 17 95 61 6f',
+ 3072: '4e 0a 8c 6f 79 34 a7 88 e2 26 5e 81 d6 d0 c8 f4',
+ 4080: '43 8d d5 ea fe a0 11 1b 6f 36 b4 b9 38 da 2a 68',
+ 4096: '5f 6b fc 73 81 58 74 d9 71 00 f0 86 97 93 57 d8'
+ }
+ ),
+ # Page 9
+ (
+ 'ebb46227c6cc8b37641910833222772a',
+ {
+ 0: '72 0c 94 b6 3e df 44 e1 31 d9 50 ca 21 1a 5a 30',
+ 16: 'c3 66 fd ea cf 9c a8 04 36 be 7c 35 84 24 d2 0b',
+ 240: 'b3 39 4a 40 aa bf 75 cb a4 22 82 ef 25 a0 05 9f',
+ 256: '48 47 d8 1d a4 94 2d bc 24 9d ef c4 8c 92 2b 9f',
+ 496: '08 12 8c 46 9f 27 53 42 ad da 20 2b 2b 58 da 95',
+ 512: '97 0d ac ef 40 ad 98 72 3b ac 5d 69 55 b8 17 61',
+ 752: '3c b8 99 93 b0 7b 0c ed 93 de 13 d2 a1 10 13 ac',
+ 768: 'ef 2d 67 6f 15 45 c2 c1 3d c6 80 a0 2f 4a db fe',
+ 1008: 'b6 05 95 51 4f 24 bc 9f e5 22 a6 ca d7 39 36 44',
+ 1024: 'b5 15 a8 c5 01 17 54 f5 90 03 05 8b db 81 51 4e',
+ 1520: '3c 70 04 7e 8c bc 03 8e 3b 98 20 db 60 1d a4 95',
+ 1536: '11 75 da 6e e7 56 de 46 a5 3e 2b 07 56 60 b7 70',
+ 2032: '00 a5 42 bb a0 21 11 cc 2c 65 b3 8e bd ba 58 7e',
+ 2048: '58 65 fd bb 5b 48 06 41 04 e8 30 b3 80 f2 ae de',
+ 3056: '34 b2 1a d2 ad 44 e9 99 db 2d 7f 08 63 f0 d9 b6',
+ 3072: '84 a9 21 8f c3 6e 8a 5f 2c cf be ae 53 a2 7d 25',
+ 4080: 'a2 22 1a 11 b8 33 cc b4 98 a5 95 40 f0 54 5f 4a',
+ 4096: '5b be b4 78 7d 59 e5 37 3f db ea 6c 6f 75 c2 9b'
+ }
+ ),
+ (
+ 'c109163908ebe51debb46227c6cc8b37641910833222772a',
+ {
+ 0: '54 b6 4e 6b 5a 20 b5 e2 ec 84 59 3d c7 98 9d a7',
+ 16: 'c1 35 ee e2 37 a8 54 65 ff 97 dc 03 92 4f 45 ce',
+ 240: 'cf cc 92 2f b4 a1 4a b4 5d 61 75 aa bb f2 d2 01',
+ 256: '83 7b 87 e2 a4 46 ad 0e f7 98 ac d0 2b 94 12 4f',
+ 496: '17 a6 db d6 64 92 6a 06 36 b3 f4 c3 7a 4f 46 94',
+ 512: '4a 5f 9f 26 ae ee d4 d4 a2 5f 63 2d 30 52 33 d9',
+ 752: '80 a3 d0 1e f0 0c 8e 9a 42 09 c1 7f 4e eb 35 8c',
+ 768: 'd1 5e 7d 5f fa aa bc 02 07 bf 20 0a 11 77 93 a2',
+ 1008: '34 96 82 bf 58 8e aa 52 d0 aa 15 60 34 6a ea fa',
+ 1024: 'f5 85 4c db 76 c8 89 e3 ad 63 35 4e 5f 72 75 e3',
+ 1520: '53 2c 7c ec cb 39 df 32 36 31 84 05 a4 b1 27 9c',
+ 1536: 'ba ef e6 d9 ce b6 51 84 22 60 e0 d1 e0 5e 3b 90',
+ 2032: 'e8 2d 8c 6d b5 4e 3c 63 3f 58 1c 95 2b a0 42 07',
+ 2048: '4b 16 e5 0a bd 38 1b d7 09 00 a9 cd 9a 62 cb 23',
+ 3056: '36 82 ee 33 bd 14 8b d9 f5 86 56 cd 8f 30 d9 fb',
+ 3072: '1e 5a 0b 84 75 04 5d 9b 20 b2 62 86 24 ed fd 9e',
+ 4080: '63 ed d6 84 fb 82 62 82 fe 52 8f 9c 0e 92 37 bc',
+ 4096: 'e4 dd 2e 98 d6 96 0f ae 0b 43 54 54 56 74 33 91'
+ }
+ ),
+ # Page 10
+ (
+ '1ada31d5cf688221c109163908ebe51debb46227c6cc8b37641910833222772a',
+ {
+ 0: 'dd 5b cb 00 18 e9 22 d4 94 75 9d 7c 39 5d 02 d3',
+ 16: 'c8 44 6f 8f 77 ab f7 37 68 53 53 eb 89 a1 c9 eb',
+ 240: 'af 3e 30 f9 c0 95 04 59 38 15 15 75 c3 fb 90 98',
+ 256: 'f8 cb 62 74 db 99 b8 0b 1d 20 12 a9 8e d4 8f 0e',
+ 496: '25 c3 00 5a 1c b8 5d e0 76 25 98 39 ab 71 98 ab',
+ 512: '9d cb c1 83 e8 cb 99 4b 72 7b 75 be 31 80 76 9c',
+ 752: 'a1 d3 07 8d fa 91 69 50 3e d9 d4 49 1d ee 4e b2',
+ 768: '85 14 a5 49 58 58 09 6f 59 6e 4b cd 66 b1 06 65',
+ 1008: '5f 40 d5 9e c1 b0 3b 33 73 8e fa 60 b2 25 5d 31',
+ 1024: '34 77 c7 f7 64 a4 1b ac ef f9 0b f1 4f 92 b7 cc',
+ 1520: 'ac 4e 95 36 8d 99 b9 eb 78 b8 da 8f 81 ff a7 95',
+ 1536: '8c 3c 13 f8 c2 38 8b b7 3f 38 57 6e 65 b7 c4 46',
+ 2032: '13 c4 b9 c1 df b6 65 79 ed dd 8a 28 0b 9f 73 16',
+ 2048: 'dd d2 78 20 55 01 26 69 8e fa ad c6 4b 64 f6 6e',
+ 3056: 'f0 8f 2e 66 d2 8e d1 43 f3 a2 37 cf 9d e7 35 59',
+ 3072: '9e a3 6c 52 55 31 b8 80 ba 12 43 34 f5 7b 0b 70',
+ 4080: 'd5 a3 9e 3d fc c5 02 80 ba c4 a6 b5 aa 0d ca 7d',
+ 4096: '37 0b 1c 1f e6 55 91 6d 97 fd 0d 47 ca 1d 72 b8'
+ }
+ )
+ ]
+
+ def test_keystream(self):
+ for tv in self.rfc6229_data:
+ key = unhexlify(b((tv[0])))
+ cipher = ARC4.new(key)
+ count = 0
+ for offset in range(0, 4096+1, 16):
+ ct = cipher.encrypt(b('\x00')*16)
+ expected = tv[1].get(offset)
+ if expected:
+ expected = unhexlify(b(expected.replace(" ", '')))
+ self.assertEqual(ct, expected)
+ count += 1
+ self.assertEqual(count, len(tv[1]))
+
+
+class Drop_Tests(unittest.TestCase):
+ key = b('\xAA')*16
+ data = b('\x00')*5000
+
+ def setUp(self):
+ self.cipher = ARC4.new(self.key)
+
+ def test_drop256_encrypt(self):
+ cipher_drop = ARC4.new(self.key, 256)
+ ct_drop = cipher_drop.encrypt(self.data[:16])
+ ct = self.cipher.encrypt(self.data)[256:256+16]
+ self.assertEqual(ct_drop, ct)
+
+ def test_drop256_decrypt(self):
+ cipher_drop = ARC4.new(self.key, 256)
+ pt_drop = cipher_drop.decrypt(self.data[:16])
+ pt = self.cipher.decrypt(self.data)[256:256+16]
+ self.assertEqual(pt_drop, pt)
+
+
+class KeyLength(unittest.TestCase):
+
+ def runTest(self):
+ self.assertRaises(ValueError, ARC4.new, b'')
+ self.assertRaises(ValueError, ARC4.new, b'\x00' * 257)
+
+
+def get_tests(config={}):
+ from .common import make_stream_tests
+ tests = make_stream_tests(ARC4, "ARC4", test_data)
+ tests += list_test_cases(RFC6229_Tests)
+ tests += list_test_cases(Drop_Tests)
+ tests.append(KeyLength())
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_Blowfish.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_Blowfish.py
new file mode 100644
index 00000000..4ce3a415
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_Blowfish.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/test_Blowfish.py: Self-test for the Blowfish cipher
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Cipher.Blowfish"""
+
+import unittest
+
+from Crypto.Util.py3compat import bchr
+
+from Crypto.Cipher import Blowfish
+
+# This is a list of (plaintext, ciphertext, key) tuples.
+test_data = [
+ # Test vectors from http://www.schneier.com/code/vectors.txt
+ ('0000000000000000', '4ef997456198dd78', '0000000000000000'),
+ ('ffffffffffffffff', '51866fd5b85ecb8a', 'ffffffffffffffff'),
+ ('1000000000000001', '7d856f9a613063f2', '3000000000000000'),
+ ('1111111111111111', '2466dd878b963c9d', '1111111111111111'),
+ ('1111111111111111', '61f9c3802281b096', '0123456789abcdef'),
+ ('0123456789abcdef', '7d0cc630afda1ec7', '1111111111111111'),
+ ('0000000000000000', '4ef997456198dd78', '0000000000000000'),
+ ('0123456789abcdef', '0aceab0fc6a0a28d', 'fedcba9876543210'),
+ ('01a1d6d039776742', '59c68245eb05282b', '7ca110454a1a6e57'),
+ ('5cd54ca83def57da', 'b1b8cc0b250f09a0', '0131d9619dc1376e'),
+ ('0248d43806f67172', '1730e5778bea1da4', '07a1133e4a0b2686'),
+ ('51454b582ddf440a', 'a25e7856cf2651eb', '3849674c2602319e'),
+ ('42fd443059577fa2', '353882b109ce8f1a', '04b915ba43feb5b6'),
+ ('059b5e0851cf143a', '48f4d0884c379918', '0113b970fd34f2ce'),
+ ('0756d8e0774761d2', '432193b78951fc98', '0170f175468fb5e6'),
+ ('762514b829bf486a', '13f04154d69d1ae5', '43297fad38e373fe'),
+ ('3bdd119049372802', '2eedda93ffd39c79', '07a7137045da2a16'),
+ ('26955f6835af609a', 'd887e0393c2da6e3', '04689104c2fd3b2f'),
+ ('164d5e404f275232', '5f99d04f5b163969', '37d06bb516cb7546'),
+ ('6b056e18759f5cca', '4a057a3b24d3977b', '1f08260d1ac2465e'),
+ ('004bd6ef09176062', '452031c1e4fada8e', '584023641aba6176'),
+ ('480d39006ee762f2', '7555ae39f59b87bd', '025816164629b007'),
+ ('437540c8698f3cfa', '53c55f9cb49fc019', '49793ebc79b3258f'),
+ ('072d43a077075292', '7a8e7bfa937e89a3', '4fb05e1515ab73a7'),
+ ('02fe55778117f12a', 'cf9c5d7a4986adb5', '49e95d6d4ca229bf'),
+ ('1d9d5c5018f728c2', 'd1abb290658bc778', '018310dc409b26d6'),
+ ('305532286d6f295a', '55cb3774d13ef201', '1c587f1c13924fef'),
+ ('0123456789abcdef', 'fa34ec4847b268b2', '0101010101010101'),
+ ('0123456789abcdef', 'a790795108ea3cae', '1f1f1f1f0e0e0e0e'),
+ ('0123456789abcdef', 'c39e072d9fac631d', 'e0fee0fef1fef1fe'),
+ ('ffffffffffffffff', '014933e0cdaff6e4', '0000000000000000'),
+ ('0000000000000000', 'f21e9a77b71c49bc', 'ffffffffffffffff'),
+ ('0000000000000000', '245946885754369a', '0123456789abcdef'),
+ ('ffffffffffffffff', '6b5c5a9c5d9e0a5a', 'fedcba9876543210'),
+ #('fedcba9876543210', 'f9ad597c49db005e', 'f0'),
+ #('fedcba9876543210', 'e91d21c1d961a6d6', 'f0e1'),
+ #('fedcba9876543210', 'e9c2b70a1bc65cf3', 'f0e1d2'),
+ ('fedcba9876543210', 'be1e639408640f05', 'f0e1d2c3'),
+ ('fedcba9876543210', 'b39e44481bdb1e6e', 'f0e1d2c3b4'),
+ ('fedcba9876543210', '9457aa83b1928c0d', 'f0e1d2c3b4a5'),
+ ('fedcba9876543210', '8bb77032f960629d', 'f0e1d2c3b4a596'),
+ ('fedcba9876543210', 'e87a244e2cc85e82', 'f0e1d2c3b4a59687'),
+ ('fedcba9876543210', '15750e7a4f4ec577', 'f0e1d2c3b4a5968778'),
+ ('fedcba9876543210', '122ba70b3ab64ae0', 'f0e1d2c3b4a596877869'),
+ ('fedcba9876543210', '3a833c9affc537f6', 'f0e1d2c3b4a5968778695a'),
+ ('fedcba9876543210', '9409da87a90f6bf2', 'f0e1d2c3b4a5968778695a4b'),
+ ('fedcba9876543210', '884f80625060b8b4', 'f0e1d2c3b4a5968778695a4b3c'),
+ ('fedcba9876543210', '1f85031c19e11968', 'f0e1d2c3b4a5968778695a4b3c2d'),
+ ('fedcba9876543210', '79d9373a714ca34f', 'f0e1d2c3b4a5968778695a4b3c2d1e'),
+ ('fedcba9876543210', '93142887ee3be15c',
+ 'f0e1d2c3b4a5968778695a4b3c2d1e0f'),
+ ('fedcba9876543210', '03429e838ce2d14b',
+ 'f0e1d2c3b4a5968778695a4b3c2d1e0f00'),
+ ('fedcba9876543210', 'a4299e27469ff67b',
+ 'f0e1d2c3b4a5968778695a4b3c2d1e0f0011'),
+ ('fedcba9876543210', 'afd5aed1c1bc96a8',
+ 'f0e1d2c3b4a5968778695a4b3c2d1e0f001122'),
+ ('fedcba9876543210', '10851c0e3858da9f',
+ 'f0e1d2c3b4a5968778695a4b3c2d1e0f00112233'),
+ ('fedcba9876543210', 'e6f51ed79b9db21f',
+ 'f0e1d2c3b4a5968778695a4b3c2d1e0f0011223344'),
+ ('fedcba9876543210', '64a6e14afd36b46f',
+ 'f0e1d2c3b4a5968778695a4b3c2d1e0f001122334455'),
+ ('fedcba9876543210', '80c7d7d45a5479ad',
+ 'f0e1d2c3b4a5968778695a4b3c2d1e0f00112233445566'),
+ ('fedcba9876543210', '05044b62fa52d080',
+ 'f0e1d2c3b4a5968778695a4b3c2d1e0f0011223344556677'),
+]
+
+
+class KeyLength(unittest.TestCase):
+
+ def runTest(self):
+ self.assertRaises(ValueError, Blowfish.new, bchr(0) * 3,
+ Blowfish.MODE_ECB)
+ self.assertRaises(ValueError, Blowfish.new, bchr(0) * 57,
+ Blowfish.MODE_ECB)
+
+
+class TestOutput(unittest.TestCase):
+
+ def runTest(self):
+ # Encrypt/Decrypt data and test output parameter
+
+ cipher = Blowfish.new(b'4'*16, Blowfish.MODE_ECB)
+
+ pt = b'5' * 16
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(16)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ output = memoryview(bytearray(16))
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0'*16)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0'*16)
+
+ shorter_output = bytearray(7)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+def get_tests(config={}):
+ from .common import make_block_tests
+ tests = make_block_tests(Blowfish, "Blowfish", test_data)
+ tests.append(KeyLength())
+ tests += [TestOutput()]
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CAST.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CAST.py
new file mode 100644
index 00000000..ff13bd4a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CAST.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/CAST.py: Self-test for the CAST-128 (CAST5) cipher
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Cipher.CAST"""
+
+import unittest
+
+from Crypto.Util.py3compat import bchr
+
+from Crypto.Cipher import CAST
+
+# This is a list of (plaintext, ciphertext, key) tuples.
+test_data = [
+ # Test vectors from RFC 2144, B.1
+ ('0123456789abcdef', '238b4fe5847e44b2',
+ '0123456712345678234567893456789a',
+ '128-bit key'),
+
+ ('0123456789abcdef', 'eb6a711a2c02271b',
+ '01234567123456782345',
+ '80-bit key'),
+
+ ('0123456789abcdef', '7ac816d16e9b302e',
+ '0123456712',
+ '40-bit key'),
+]
+
+
+class KeyLength(unittest.TestCase):
+
+ def runTest(self):
+ self.assertRaises(ValueError, CAST.new, bchr(0) * 4, CAST.MODE_ECB)
+ self.assertRaises(ValueError, CAST.new, bchr(0) * 17, CAST.MODE_ECB)
+
+
+class TestOutput(unittest.TestCase):
+
+ def runTest(self):
+ # Encrypt/Decrypt data and test output parameter
+
+ cipher = CAST.new(b'4'*16, CAST.MODE_ECB)
+
+ pt = b'5' * 16
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(16)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ output = memoryview(bytearray(16))
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0'*16)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0'*16)
+
+ shorter_output = bytearray(7)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+def get_tests(config={}):
+ from .common import make_block_tests
+
+ tests = make_block_tests(CAST, "CAST", test_data)
+ tests.append(KeyLength())
+ tests.append(TestOutput())
+ return tests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CBC.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CBC.py
new file mode 100644
index 00000000..374fb5a4
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CBC.py
@@ -0,0 +1,556 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Util.py3compat import tobytes, is_string
+from Crypto.Cipher import AES, DES3, DES
+from Crypto.Hash import SHAKE128
+
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+class BlockChainingTests(unittest.TestCase):
+
+ key_128 = get_tag_random("key_128", 16)
+ key_192 = get_tag_random("key_192", 24)
+ iv_128 = get_tag_random("iv_128", 16)
+ iv_64 = get_tag_random("iv_64", 8)
+ data_128 = get_tag_random("data_128", 16)
+
+ def test_loopback_128(self):
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ pt = get_tag_random("plaintext", 16 * 100)
+ ct = cipher.encrypt(pt)
+
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_loopback_64(self):
+ cipher = DES3.new(self.key_192, self.des3_mode, self.iv_64)
+ pt = get_tag_random("plaintext", 8 * 100)
+ ct = cipher.encrypt(pt)
+
+ cipher = DES3.new(self.key_192, self.des3_mode, self.iv_64)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_iv(self):
+ # If not passed, the iv is created randomly
+ cipher = AES.new(self.key_128, self.aes_mode)
+ iv1 = cipher.iv
+ cipher = AES.new(self.key_128, self.aes_mode)
+ iv2 = cipher.iv
+ self.assertNotEqual(iv1, iv2)
+ self.assertEqual(len(iv1), 16)
+
+ # IV can be passed in uppercase or lowercase
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ ct = cipher.encrypt(self.data_128)
+
+ cipher = AES.new(self.key_128, self.aes_mode, iv=self.iv_128)
+ self.assertEqual(ct, cipher.encrypt(self.data_128))
+
+ cipher = AES.new(self.key_128, self.aes_mode, IV=self.iv_128)
+ self.assertEqual(ct, cipher.encrypt(self.data_128))
+
+ def test_iv_must_be_bytes(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, self.aes_mode,
+ iv = u'test1234567890-*')
+
+ def test_only_one_iv(self):
+ # Only one IV/iv keyword allowed
+ self.assertRaises(TypeError, AES.new, self.key_128, self.aes_mode,
+ iv=self.iv_128, IV=self.iv_128)
+
+ def test_iv_with_matching_length(self):
+ self.assertRaises(ValueError, AES.new, self.key_128, self.aes_mode,
+ b"")
+ self.assertRaises(ValueError, AES.new, self.key_128, self.aes_mode,
+ self.iv_128[:15])
+ self.assertRaises(ValueError, AES.new, self.key_128, self.aes_mode,
+ self.iv_128 + b"0")
+
+ def test_block_size_128(self):
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ self.assertEqual(cipher.block_size, AES.block_size)
+
+ def test_block_size_64(self):
+ cipher = DES3.new(self.key_192, self.des3_mode, self.iv_64)
+ self.assertEqual(cipher.block_size, DES3.block_size)
+
+ def test_unaligned_data_128(self):
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ for wrong_length in range(1,16):
+ self.assertRaises(ValueError, cipher.encrypt, b"5" * wrong_length)
+
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ for wrong_length in range(1,16):
+ self.assertRaises(ValueError, cipher.decrypt, b"5" * wrong_length)
+
+ def test_unaligned_data_64(self):
+ cipher = DES3.new(self.key_192, self.des3_mode, self.iv_64)
+ for wrong_length in range(1,8):
+ self.assertRaises(ValueError, cipher.encrypt, b"5" * wrong_length)
+
+ cipher = DES3.new(self.key_192, self.des3_mode, self.iv_64)
+ for wrong_length in range(1,8):
+ self.assertRaises(ValueError, cipher.decrypt, b"5" * wrong_length)
+
+ def test_IV_iv_attributes(self):
+ data = get_tag_random("data", 16 * 100)
+ for func in "encrypt", "decrypt":
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ getattr(cipher, func)(data)
+ self.assertEqual(cipher.iv, self.iv_128)
+ self.assertEqual(cipher.IV, self.iv_128)
+
+ def test_unknown_parameters(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, self.aes_mode,
+ self.iv_128, 7)
+ self.assertRaises(TypeError, AES.new, self.key_128, self.aes_mode,
+ iv=self.iv_128, unknown=7)
+ # But some are only known by the base cipher (e.g. use_aesni consumed by the AES module)
+ AES.new(self.key_128, self.aes_mode, iv=self.iv_128, use_aesni=False)
+
+ def test_null_encryption_decryption(self):
+ for func in "encrypt", "decrypt":
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ result = getattr(cipher, func)(b"")
+ self.assertEqual(result, b"")
+
+ def test_either_encrypt_or_decrypt(self):
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ cipher.encrypt(b"")
+ self.assertRaises(TypeError, cipher.decrypt, b"")
+
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ cipher.decrypt(b"")
+ self.assertRaises(TypeError, cipher.encrypt, b"")
+
+ def test_data_must_be_bytes(self):
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ self.assertRaises(TypeError, cipher.encrypt, u'test1234567890-*')
+
+ cipher = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ self.assertRaises(TypeError, cipher.decrypt, u'test1234567890-*')
+
+ def test_bytearray(self):
+ data = b"1" * 128
+ data_ba = bytearray(data)
+
+ # Encrypt
+ key_ba = bytearray(self.key_128)
+ iv_ba = bytearray(self.iv_128)
+
+ cipher1 = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ ref1 = cipher1.encrypt(data)
+
+ cipher2 = AES.new(key_ba, self.aes_mode, iv_ba)
+ key_ba[:3] = b'\xFF\xFF\xFF'
+ iv_ba[:3] = b'\xFF\xFF\xFF'
+ ref2 = cipher2.encrypt(data_ba)
+
+ self.assertEqual(ref1, ref2)
+ self.assertEqual(cipher1.iv, cipher2.iv)
+
+ # Decrypt
+ key_ba = bytearray(self.key_128)
+ iv_ba = bytearray(self.iv_128)
+
+ cipher3 = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ ref3 = cipher3.decrypt(data)
+
+ cipher4 = AES.new(key_ba, self.aes_mode, iv_ba)
+ key_ba[:3] = b'\xFF\xFF\xFF'
+ iv_ba[:3] = b'\xFF\xFF\xFF'
+ ref4 = cipher4.decrypt(data_ba)
+
+ self.assertEqual(ref3, ref4)
+
+ def test_memoryview(self):
+ data = b"1" * 128
+ data_mv = memoryview(bytearray(data))
+
+ # Encrypt
+ key_mv = memoryview(bytearray(self.key_128))
+ iv_mv = memoryview(bytearray(self.iv_128))
+
+ cipher1 = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ ref1 = cipher1.encrypt(data)
+
+ cipher2 = AES.new(key_mv, self.aes_mode, iv_mv)
+ key_mv[:3] = b'\xFF\xFF\xFF'
+ iv_mv[:3] = b'\xFF\xFF\xFF'
+ ref2 = cipher2.encrypt(data_mv)
+
+ self.assertEqual(ref1, ref2)
+ self.assertEqual(cipher1.iv, cipher2.iv)
+
+ # Decrypt
+ key_mv = memoryview(bytearray(self.key_128))
+ iv_mv = memoryview(bytearray(self.iv_128))
+
+ cipher3 = AES.new(self.key_128, self.aes_mode, self.iv_128)
+ ref3 = cipher3.decrypt(data)
+
+ cipher4 = AES.new(key_mv, self.aes_mode, iv_mv)
+ key_mv[:3] = b'\xFF\xFF\xFF'
+ iv_mv[:3] = b'\xFF\xFF\xFF'
+ ref4 = cipher4.decrypt(data_mv)
+
+ self.assertEqual(ref3, ref4)
+
+ def test_output_param(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(128)
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+
+ def test_output_param_same_buffer(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ ct = cipher.encrypt(pt)
+
+ pt_ba = bytearray(pt)
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ res = cipher.encrypt(pt_ba, output=pt_ba)
+ self.assertEqual(ct, pt_ba)
+ self.assertEqual(res, None)
+
+ ct_ba = bytearray(ct)
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ res = cipher.decrypt(ct_ba, output=ct_ba)
+ self.assertEqual(pt, ct_ba)
+ self.assertEqual(res, None)
+
+
+ def test_output_param_memoryview(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ ct = cipher.encrypt(pt)
+
+ output = memoryview(bytearray(128))
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ def test_output_param_neg(self):
+ LEN_PT = 128
+
+ pt = b'5' * LEN_PT
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ ct = cipher.encrypt(pt)
+
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0' * LEN_PT)
+
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0' * LEN_PT)
+
+ shorter_output = bytearray(LEN_PT - 1)
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ cipher = AES.new(b'4'*16, self.aes_mode, iv=self.iv_128)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+class CbcTests(BlockChainingTests):
+ aes_mode = AES.MODE_CBC
+ des3_mode = DES3.MODE_CBC
+
+
+class NistBlockChainingVectors(unittest.TestCase):
+
+ def _do_kat_aes_test(self, file_name):
+
+ test_vectors = load_test_vectors(("Cipher", "AES"),
+ file_name,
+ "AES CBC KAT",
+ { "count" : lambda x: int(x) } )
+ if test_vectors is None:
+ return
+
+ direction = None
+ for tv in test_vectors:
+
+ # The test vector file contains some directive lines
+ if is_string(tv):
+ direction = tv
+ continue
+
+ self.description = tv.desc
+
+ cipher = AES.new(tv.key, self.aes_mode, tv.iv)
+ if direction == "[ENCRYPT]":
+ self.assertEqual(cipher.encrypt(tv.plaintext), tv.ciphertext)
+ elif direction == "[DECRYPT]":
+ self.assertEqual(cipher.decrypt(tv.ciphertext), tv.plaintext)
+ else:
+ assert False
+
+ # See Section 6.4.2 in AESAVS
+ def _do_mct_aes_test(self, file_name):
+
+ test_vectors = load_test_vectors(("Cipher", "AES"),
+ file_name,
+ "AES CBC Montecarlo",
+ { "count" : lambda x: int(x) } )
+ if test_vectors is None:
+ return
+
+ direction = None
+ for tv in test_vectors:
+
+ # The test vector file contains some directive lines
+ if is_string(tv):
+ direction = tv
+ continue
+
+ self.description = tv.desc
+ cipher = AES.new(tv.key, self.aes_mode, tv.iv)
+
+ if direction == '[ENCRYPT]':
+ cts = [ tv.iv ]
+ for count in range(1000):
+ cts.append(cipher.encrypt(tv.plaintext))
+ tv.plaintext = cts[-2]
+ self.assertEqual(cts[-1], tv.ciphertext)
+ elif direction == '[DECRYPT]':
+ pts = [ tv.iv]
+ for count in range(1000):
+ pts.append(cipher.decrypt(tv.ciphertext))
+ tv.ciphertext = pts[-2]
+ self.assertEqual(pts[-1], tv.plaintext)
+ else:
+ assert False
+
+ def _do_tdes_test(self, file_name):
+
+ test_vectors = load_test_vectors(("Cipher", "TDES"),
+ file_name,
+ "TDES CBC KAT",
+ { "count" : lambda x: int(x) } )
+ if test_vectors is None:
+ return
+
+ direction = None
+ for tv in test_vectors:
+
+ # The test vector file contains some directive lines
+ if is_string(tv):
+ direction = tv
+ continue
+
+ self.description = tv.desc
+ if hasattr(tv, "keys"):
+ cipher = DES.new(tv.keys, self.des_mode, tv.iv)
+ else:
+ if tv.key1 != tv.key3:
+ key = tv.key1 + tv.key2 + tv.key3 # Option 3
+ else:
+ key = tv.key1 + tv.key2 # Option 2
+ cipher = DES3.new(key, self.des3_mode, tv.iv)
+
+ if direction == "[ENCRYPT]":
+ self.assertEqual(cipher.encrypt(tv.plaintext), tv.ciphertext)
+ elif direction == "[DECRYPT]":
+ self.assertEqual(cipher.decrypt(tv.ciphertext), tv.plaintext)
+ else:
+ assert False
+
+
+class NistCbcVectors(NistBlockChainingVectors):
+ aes_mode = AES.MODE_CBC
+ des_mode = DES.MODE_CBC
+ des3_mode = DES3.MODE_CBC
+
+
+# Create one test method per file
+nist_aes_kat_mmt_files = (
+ # KAT
+ "CBCGFSbox128.rsp",
+ "CBCGFSbox192.rsp",
+ "CBCGFSbox256.rsp",
+ "CBCKeySbox128.rsp",
+ "CBCKeySbox192.rsp",
+ "CBCKeySbox256.rsp",
+ "CBCVarKey128.rsp",
+ "CBCVarKey192.rsp",
+ "CBCVarKey256.rsp",
+ "CBCVarTxt128.rsp",
+ "CBCVarTxt192.rsp",
+ "CBCVarTxt256.rsp",
+ # MMT
+ "CBCMMT128.rsp",
+ "CBCMMT192.rsp",
+ "CBCMMT256.rsp",
+ )
+nist_aes_mct_files = (
+ "CBCMCT128.rsp",
+ "CBCMCT192.rsp",
+ "CBCMCT256.rsp",
+ )
+
+for file_name in nist_aes_kat_mmt_files:
+ def new_func(self, file_name=file_name):
+ self._do_kat_aes_test(file_name)
+ setattr(NistCbcVectors, "test_AES_" + file_name, new_func)
+
+for file_name in nist_aes_mct_files:
+ def new_func(self, file_name=file_name):
+ self._do_mct_aes_test(file_name)
+ setattr(NistCbcVectors, "test_AES_" + file_name, new_func)
+del file_name, new_func
+
+nist_tdes_files = (
+ "TCBCMMT2.rsp", # 2TDES
+ "TCBCMMT3.rsp", # 3TDES
+ "TCBCinvperm.rsp", # Single DES
+ "TCBCpermop.rsp",
+ "TCBCsubtab.rsp",
+ "TCBCvarkey.rsp",
+ "TCBCvartext.rsp",
+ )
+
+for file_name in nist_tdes_files:
+ def new_func(self, file_name=file_name):
+ self._do_tdes_test(file_name)
+ setattr(NistCbcVectors, "test_TDES_" + file_name, new_func)
+
+# END OF NIST CBC TEST VECTORS
+
+
+class SP800TestVectors(unittest.TestCase):
+ """Class exercising the CBC test vectors found in Section F.2
+ of NIST SP 800-3A"""
+
+ def test_aes_128(self):
+ key = '2b7e151628aed2a6abf7158809cf4f3c'
+ iv = '000102030405060708090a0b0c0d0e0f'
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = '7649abac8119b246cee98e9b12e9197d' +\
+ '5086cb9b507219ee95db113a917678b2' +\
+ '73bed6b8e3c1743b7116e69e22229516' +\
+ '3ff1caa1681fac09120eca307586e1a7'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ def test_aes_192(self):
+ key = '8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b'
+ iv = '000102030405060708090a0b0c0d0e0f'
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = '4f021db243bc633d7178183a9fa071e8' +\
+ 'b4d9ada9ad7dedf4e5e738763f69145a' +\
+ '571b242012fb7ae07fa9baac3df102e0' +\
+ '08b0e27988598881d920a9e64f5615cd'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ def test_aes_256(self):
+ key = '603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4'
+ iv = '000102030405060708090a0b0c0d0e0f'
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = 'f58c4c04d6e5f1ba779eabfb5f7bfbd6' +\
+ '9cfc4e967edb808d679f777bc6702c7d' +\
+ '39f23369a9d9bacfa530e26304231461' +\
+ 'b2eb05e2c39be9fcda6c19078c6a9d1b'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CBC, iv)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(CbcTests)
+ if config.get('slow_tests'):
+ tests += list_test_cases(NistCbcVectors)
+ tests += list_test_cases(SP800TestVectors)
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CCM.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CCM.py
new file mode 100644
index 00000000..23b89898
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CCM.py
@@ -0,0 +1,970 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors_wycheproof
+from Crypto.Util.py3compat import tobytes, bchr
+from Crypto.Cipher import AES
+from Crypto.Hash import SHAKE128
+
+from Crypto.Util.strxor import strxor
+
+from Crypto.Cipher._mode_ccm import CCMMessageTooLongError
+
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+
+class CcmTests(unittest.TestCase):
+
+ key_128 = get_tag_random("key_128", 16)
+ nonce_96 = get_tag_random("nonce_128", 12)
+ data = get_tag_random("data", 128)
+
+ def test_loopback_128(self):
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ pt = get_tag_random("plaintext", 16 * 100)
+ ct = cipher.encrypt(pt)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_nonce(self):
+ # If not passed, the nonce is created randomly
+ cipher = AES.new(self.key_128, AES.MODE_CCM)
+ nonce1 = cipher.nonce
+ cipher = AES.new(self.key_128, AES.MODE_CCM)
+ nonce2 = cipher.nonce
+ self.assertEqual(len(nonce1), 11)
+ self.assertNotEqual(nonce1, nonce2)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, self.nonce_96)
+ ct = cipher.encrypt(self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ self.assertEqual(ct, cipher.encrypt(self.data))
+
+ def test_nonce_must_be_bytes(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_CCM,
+ nonce=u'test12345678')
+
+ def test_nonce_length(self):
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CCM,
+ nonce=b"")
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CCM,
+ nonce=bchr(1) * 6)
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CCM,
+ nonce=bchr(1) * 14)
+ for x in range(7, 13 + 1):
+ AES.new(self.key_128, AES.MODE_CCM, nonce=bchr(1) * x)
+
+ def test_block_size(self):
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ self.assertEqual(cipher.block_size, AES.block_size)
+
+ def test_nonce_attribute(self):
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ self.assertEqual(cipher.nonce, self.nonce_96)
+
+ # By default, a 11 bytes long nonce is randomly generated
+ nonce1 = AES.new(self.key_128, AES.MODE_CCM).nonce
+ nonce2 = AES.new(self.key_128, AES.MODE_CCM).nonce
+ self.assertEqual(len(nonce1), 11)
+ self.assertNotEqual(nonce1, nonce2)
+
+ def test_unknown_parameters(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_CCM,
+ self.nonce_96, 7)
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96, unknown=7)
+
+ # But some are only known by the base cipher
+ # (e.g. use_aesni consumed by the AES module)
+ AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ use_aesni=False)
+
+ def test_null_encryption_decryption(self):
+ for func in "encrypt", "decrypt":
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ result = getattr(cipher, func)(b"")
+ self.assertEqual(result, b"")
+
+ def test_either_encrypt_or_decrypt(self):
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.encrypt(b"")
+ self.assertRaises(TypeError, cipher.decrypt, b"")
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.decrypt(b"")
+ self.assertRaises(TypeError, cipher.encrypt, b"")
+
+ def test_data_must_be_bytes(self):
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt, u'test1234567890-*')
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt, u'test1234567890-*')
+
+ def test_mac_len(self):
+ # Invalid MAC length
+ for mac_len in range(3, 17 + 1, 2):
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96, mac_len=mac_len)
+
+ # Valid MAC length
+ for mac_len in range(4, 16 + 1, 2):
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ mac_len=mac_len)
+ _, mac = cipher.encrypt_and_digest(self.data)
+ self.assertEqual(len(mac), mac_len)
+
+ # Default MAC length
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ _, mac = cipher.encrypt_and_digest(self.data)
+ self.assertEqual(len(mac), 16)
+
+ def test_invalid_mac(self):
+ from Crypto.Util.strxor import strxor_c
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ ct, mac = cipher.encrypt_and_digest(self.data)
+
+ invalid_mac = strxor_c(mac, 0x01)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct,
+ invalid_mac)
+
+ def test_hex_mac(self):
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ mac_hex = cipher.hexdigest()
+ self.assertEqual(cipher.digest(), unhexlify(mac_hex))
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.hexverify(mac_hex)
+
+ def test_longer_assoc_data_than_declared(self):
+ # More than zero
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ assoc_len=0)
+ self.assertRaises(ValueError, cipher.update, b"1")
+
+ # Too large
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ assoc_len=15)
+ self.assertRaises(ValueError, cipher.update, self.data)
+
+ def test_shorter_assoc_data_than_expected(self):
+ DATA_LEN = len(self.data)
+
+ # With plaintext
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ assoc_len=DATA_LEN + 1)
+ cipher.update(self.data)
+ self.assertRaises(ValueError, cipher.encrypt, self.data)
+
+ # With empty plaintext
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ assoc_len=DATA_LEN + 1)
+ cipher.update(self.data)
+ self.assertRaises(ValueError, cipher.digest)
+
+ # With ciphertext
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ assoc_len=DATA_LEN + 1)
+ cipher.update(self.data)
+ self.assertRaises(ValueError, cipher.decrypt, self.data)
+
+ # With empty ciphertext
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.update(self.data)
+ mac = cipher.digest()
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ assoc_len=DATA_LEN + 1)
+ cipher.update(self.data)
+ self.assertRaises(ValueError, cipher.verify, mac)
+
+ def test_shorter_and_longer_plaintext_than_declared(self):
+ DATA_LEN = len(self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ msg_len=DATA_LEN + 1)
+ cipher.encrypt(self.data)
+ self.assertRaises(ValueError, cipher.digest)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ msg_len=DATA_LEN - 1)
+ self.assertRaises(ValueError, cipher.encrypt, self.data)
+
+ def test_shorter_ciphertext_than_declared(self):
+ DATA_LEN = len(self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ ct, mac = cipher.encrypt_and_digest(self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ msg_len=DATA_LEN + 1)
+ cipher.decrypt(ct)
+ self.assertRaises(ValueError, cipher.verify, mac)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ msg_len=DATA_LEN - 1)
+ self.assertRaises(ValueError, cipher.decrypt, ct)
+
+ def test_message_chunks(self):
+ # Validate that both associated data and plaintext/ciphertext
+ # can be broken up in chunks of arbitrary length
+
+ auth_data = get_tag_random("authenticated data", 127)
+ plaintext = get_tag_random("plaintext", 127)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.update(auth_data)
+ ciphertext, ref_mac = cipher.encrypt_and_digest(plaintext)
+
+ def break_up(data, chunk_length):
+ return [data[i:i+chunk_length] for i in range(0, len(data),
+ chunk_length)]
+
+ # Encryption
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ msg_len=127, assoc_len=127)
+
+ for chunk in break_up(auth_data, chunk_length):
+ cipher.update(chunk)
+ pt2 = b""
+ for chunk in break_up(ciphertext, chunk_length):
+ pt2 += cipher.decrypt(chunk)
+ self.assertEqual(plaintext, pt2)
+ cipher.verify(ref_mac)
+
+ # Decryption
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96,
+ msg_len=127, assoc_len=127)
+
+ for chunk in break_up(auth_data, chunk_length):
+ cipher.update(chunk)
+ ct2 = b""
+ for chunk in break_up(plaintext, chunk_length):
+ ct2 += cipher.encrypt(chunk)
+ self.assertEqual(ciphertext, ct2)
+ self.assertEqual(cipher.digest(), ref_mac)
+
+ def test_bytearray(self):
+
+ # Encrypt
+ key_ba = bytearray(self.key_128)
+ nonce_ba = bytearray(self.nonce_96)
+ header_ba = bytearray(self.data)
+ data_ba = bytearray(self.data)
+
+ cipher1 = AES.new(self.key_128,
+ AES.MODE_CCM,
+ nonce=self.nonce_96)
+ cipher1.update(self.data)
+ ct = cipher1.encrypt(self.data)
+ tag = cipher1.digest()
+
+ cipher2 = AES.new(key_ba,
+ AES.MODE_CCM,
+ nonce=nonce_ba)
+ key_ba[:3] = b"\xFF\xFF\xFF"
+ nonce_ba[:3] = b"\xFF\xFF\xFF"
+ cipher2.update(header_ba)
+ header_ba[:3] = b"\xFF\xFF\xFF"
+ ct_test = cipher2.encrypt(data_ba)
+ data_ba[:3] = b"\xFF\xFF\xFF"
+ tag_test = cipher2.digest()
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key_ba = bytearray(self.key_128)
+ nonce_ba = bytearray(self.nonce_96)
+ header_ba = bytearray(self.data)
+ del data_ba
+
+ cipher4 = AES.new(key_ba,
+ AES.MODE_CCM,
+ nonce=nonce_ba)
+ key_ba[:3] = b"\xFF\xFF\xFF"
+ nonce_ba[:3] = b"\xFF\xFF\xFF"
+ cipher4.update(header_ba)
+ header_ba[:3] = b"\xFF\xFF\xFF"
+ pt_test = cipher4.decrypt_and_verify(bytearray(ct_test), bytearray(tag_test))
+
+ self.assertEqual(self.data, pt_test)
+
+ def test_memoryview(self):
+
+ # Encrypt
+ key_mv = memoryview(bytearray(self.key_128))
+ nonce_mv = memoryview(bytearray(self.nonce_96))
+ header_mv = memoryview(bytearray(self.data))
+ data_mv = memoryview(bytearray(self.data))
+
+ cipher1 = AES.new(self.key_128,
+ AES.MODE_CCM,
+ nonce=self.nonce_96)
+ cipher1.update(self.data)
+ ct = cipher1.encrypt(self.data)
+ tag = cipher1.digest()
+
+ cipher2 = AES.new(key_mv,
+ AES.MODE_CCM,
+ nonce=nonce_mv)
+ key_mv[:3] = b"\xFF\xFF\xFF"
+ nonce_mv[:3] = b"\xFF\xFF\xFF"
+ cipher2.update(header_mv)
+ header_mv[:3] = b"\xFF\xFF\xFF"
+ ct_test = cipher2.encrypt(data_mv)
+ data_mv[:3] = b"\xFF\xFF\xFF"
+ tag_test = cipher2.digest()
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key_mv = memoryview(bytearray(self.key_128))
+ nonce_mv = memoryview(bytearray(self.nonce_96))
+ header_mv = memoryview(bytearray(self.data))
+ del data_mv
+
+ cipher4 = AES.new(key_mv,
+ AES.MODE_CCM,
+ nonce=nonce_mv)
+ key_mv[:3] = b"\xFF\xFF\xFF"
+ nonce_mv[:3] = b"\xFF\xFF\xFF"
+ cipher4.update(header_mv)
+ header_mv[:3] = b"\xFF\xFF\xFF"
+ pt_test = cipher4.decrypt_and_verify(memoryview(ct_test), memoryview(tag_test))
+
+ self.assertEqual(self.data, pt_test)
+
+ def test_output_param(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ ct = cipher.encrypt(pt)
+ tag = cipher.digest()
+
+ output = bytearray(128)
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ res, tag_out = cipher.encrypt_and_digest(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+ self.assertEqual(tag, tag_out)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ res = cipher.decrypt_and_verify(ct, tag, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ def test_output_param_memoryview(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ ct = cipher.encrypt(pt)
+
+ output = memoryview(bytearray(128))
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ def test_output_param_neg(self):
+
+ pt = b'5' * 16
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ ct = cipher.encrypt(pt)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0'*16)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0'*16)
+
+ shorter_output = bytearray(15)
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+ def test_message_too_long(self):
+
+ nonce = b'N' * 13
+ self.assertRaises(CCMMessageTooLongError,
+ AES.new,
+ self.key_128,
+ AES.MODE_CCM,
+ nonce=nonce,
+ assoc_len=20,
+ msg_len=0x10000)
+
+ nonce = b'N' * 7
+ self.assertRaises(CCMMessageTooLongError,
+ AES.new,
+ self.key_128,
+ AES.MODE_CCM,
+ nonce=nonce,
+ assoc_len=20,
+ msg_len=2**64)
+
+ nonce = b'N' * 13
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=nonce)
+ self.assertRaises(CCMMessageTooLongError,
+ cipher.encrypt,
+ b'C' * 0x10000)
+
+ nonce = b'N' * 13
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=nonce)
+ self.assertRaises(CCMMessageTooLongError,
+ cipher.decrypt,
+ b'C' * 0x10000)
+
+
+class CcmFSMTests(unittest.TestCase):
+
+ key_128 = get_tag_random("key_128", 16)
+ nonce_96 = get_tag_random("nonce_128", 12)
+ data = get_tag_random("data", 16)
+
+ def test_valid_init_encrypt_decrypt_digest_verify(self):
+ # No authenticated data, fixed plaintext
+ for assoc_len in (None, 0):
+ for msg_len in (None, len(self.data)):
+ # Verify path INIT->ENCRYPT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96,
+ assoc_len=assoc_len,
+ msg_len=msg_len)
+ ct = cipher.encrypt(self.data)
+ mac = cipher.digest()
+
+ # Verify path INIT->DECRYPT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96,
+ assoc_len=assoc_len,
+ msg_len=msg_len)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+
+ def test_valid_init_update_digest_verify(self):
+ # No plaintext, fixed authenticated data
+ for assoc_len in (None, len(self.data)):
+ for msg_len in (None, 0):
+ # Verify path INIT->UPDATE->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96,
+ assoc_len=assoc_len,
+ msg_len=msg_len)
+ cipher.update(self.data)
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96,
+ assoc_len=assoc_len,
+ msg_len=msg_len)
+ cipher.update(self.data)
+ cipher.verify(mac)
+
+ def test_valid_full_path(self):
+ # Fixed authenticated data, fixed plaintext
+ for assoc_len in (None, len(self.data)):
+ for msg_len in (None, len(self.data)):
+ # Verify path INIT->UPDATE->ENCRYPT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96,
+ assoc_len=assoc_len,
+ msg_len=msg_len)
+ cipher.update(self.data)
+ ct = cipher.encrypt(self.data)
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->DECRYPT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96,
+ assoc_len=assoc_len,
+ msg_len=msg_len)
+ cipher.update(self.data)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+
+ def test_valid_init_digest(self):
+ # Verify path INIT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.digest()
+
+ def test_valid_init_verify(self):
+ # Verify path INIT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ mac = cipher.digest()
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.verify(mac)
+
+ def test_valid_multiple_encrypt_or_decrypt(self):
+ # Only possible if msg_len is declared in advance
+ for method_name in "encrypt", "decrypt":
+ for auth_data in (None, b"333", self.data,
+ self.data + b"3"):
+ if auth_data is None:
+ assoc_len = None
+ else:
+ assoc_len = len(auth_data)
+ cipher = AES.new(self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96,
+ msg_len=64,
+ assoc_len=assoc_len)
+ if auth_data is not None:
+ cipher.update(auth_data)
+ method = getattr(cipher, method_name)
+ method(self.data)
+ method(self.data)
+ method(self.data)
+ method(self.data)
+
+ def test_valid_multiple_digest_or_verify(self):
+ # Multiple calls to digest
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.update(self.data)
+ first_mac = cipher.digest()
+ for x in range(4):
+ self.assertEqual(first_mac, cipher.digest())
+
+ # Multiple calls to verify
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.update(self.data)
+ for x in range(5):
+ cipher.verify(first_mac)
+
+ def test_valid_encrypt_and_digest_decrypt_and_verify(self):
+ # encrypt_and_digest
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.update(self.data)
+ ct, mac = cipher.encrypt_and_digest(self.data)
+
+ # decrypt_and_verify
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.update(self.data)
+ pt = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(self.data, pt)
+
+ def test_invalid_multiple_encrypt_decrypt_without_msg_len(self):
+ # Once per method, with or without assoc. data
+ for method_name in "encrypt", "decrypt":
+ for assoc_data_present in (True, False):
+ cipher = AES.new(self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96)
+ if assoc_data_present:
+ cipher.update(self.data)
+ method = getattr(cipher, method_name)
+ method(self.data)
+ self.assertRaises(TypeError, method, self.data)
+
+ def test_invalid_mixing_encrypt_decrypt(self):
+ # Once per method, with or without assoc. data
+ for method1_name, method2_name in (("encrypt", "decrypt"),
+ ("decrypt", "encrypt")):
+ for assoc_data_present in (True, False):
+ cipher = AES.new(self.key_128, AES.MODE_CCM,
+ nonce=self.nonce_96,
+ msg_len=32)
+ if assoc_data_present:
+ cipher.update(self.data)
+ getattr(cipher, method1_name)(self.data)
+ self.assertRaises(TypeError, getattr(cipher, method2_name),
+ self.data)
+
+ def test_invalid_encrypt_or_update_after_digest(self):
+ for method_name in "encrypt", "update":
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.encrypt(self.data)
+ cipher.digest()
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.encrypt_and_digest(self.data)
+
+ def test_invalid_decrypt_or_update_after_verify(self):
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data)
+ mac = cipher.digest()
+
+ for method_name in "decrypt", "update":
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_CCM, nonce=self.nonce_96)
+ cipher.decrypt_and_verify(ct, mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data)
+
+
+class TestVectors(unittest.TestCase):
+ """Class exercising the CCM test vectors found in Appendix C
+ of NIST SP 800-38C and in RFC 3610"""
+
+ # List of test vectors, each made up of:
+ # - authenticated data
+ # - plaintext
+ # - ciphertext
+ # - MAC
+ # - AES key
+ # - nonce
+ test_vectors_hex = [
+ # NIST SP 800 38C
+ ( '0001020304050607',
+ '20212223',
+ '7162015b',
+ '4dac255d',
+ '404142434445464748494a4b4c4d4e4f',
+ '10111213141516'),
+ ( '000102030405060708090a0b0c0d0e0f',
+ '202122232425262728292a2b2c2d2e2f',
+ 'd2a1f0e051ea5f62081a7792073d593d',
+ '1fc64fbfaccd',
+ '404142434445464748494a4b4c4d4e4f',
+ '1011121314151617'),
+ ( '000102030405060708090a0b0c0d0e0f10111213',
+ '202122232425262728292a2b2c2d2e2f3031323334353637',
+ 'e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5',
+ '484392fbc1b09951',
+ '404142434445464748494a4b4c4d4e4f',
+ '101112131415161718191a1b'),
+ ( (''.join(["%02X" % (x*16+y) for x in range(0,16) for y in range(0,16)]))*256,
+ '202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f',
+ '69915dad1e84c6376a68c2967e4dab615ae0fd1faec44cc484828529463ccf72',
+ 'b4ac6bec93e8598e7f0dadbcea5b',
+ '404142434445464748494a4b4c4d4e4f',
+ '101112131415161718191a1b1c'),
+ # RFC3610
+ ( '0001020304050607',
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e',
+ '588c979a61c663d2f066d0c2c0f989806d5f6b61dac384',
+ '17e8d12cfdf926e0',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '00000003020100a0a1a2a3a4a5'),
+ (
+ '0001020304050607',
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
+ '72c91a36e135f8cf291ca894085c87e3cc15c439c9e43a3b',
+ 'a091d56e10400916',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '00000004030201a0a1a2a3a4a5'),
+ ( '0001020304050607',
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20',
+ '51b1e5f44a197d1da46b0f8e2d282ae871e838bb64da859657',
+ '4adaa76fbd9fb0c5',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '00000005040302A0A1A2A3A4A5'),
+ ( '000102030405060708090a0b',
+ '0c0d0e0f101112131415161718191a1b1c1d1e',
+ 'a28c6865939a9a79faaa5c4c2a9d4a91cdac8c',
+ '96c861b9c9e61ef1',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '00000006050403a0a1a2a3a4a5'),
+ ( '000102030405060708090a0b',
+ '0c0d0e0f101112131415161718191a1b1c1d1e1f',
+ 'dcf1fb7b5d9e23fb9d4e131253658ad86ebdca3e',
+ '51e83f077d9c2d93',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '00000007060504a0a1a2a3a4a5'),
+ ( '000102030405060708090a0b',
+ '0c0d0e0f101112131415161718191a1b1c1d1e1f20',
+ '6fc1b011f006568b5171a42d953d469b2570a4bd87',
+ '405a0443ac91cb94',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '00000008070605a0a1a2a3a4a5'),
+ ( '0001020304050607',
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e',
+ '0135d1b2c95f41d5d1d4fec185d166b8094e999dfed96c',
+ '048c56602c97acbb7490',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '00000009080706a0a1a2a3a4a5'),
+ ( '0001020304050607',
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
+ '7b75399ac0831dd2f0bbd75879a2fd8f6cae6b6cd9b7db24',
+ 'c17b4433f434963f34b4',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '0000000a090807a0a1a2a3a4a5'),
+ ( '0001020304050607',
+ '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20',
+ '82531a60cc24945a4b8279181ab5c84df21ce7f9b73f42e197',
+ 'ea9c07e56b5eb17e5f4e',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '0000000b0a0908a0a1a2a3a4a5'),
+ ( '000102030405060708090a0b',
+ '0c0d0e0f101112131415161718191a1b1c1d1e',
+ '07342594157785152b074098330abb141b947b',
+ '566aa9406b4d999988dd',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '0000000c0b0a09a0a1a2a3a4a5'),
+ ( '000102030405060708090a0b',
+ '0c0d0e0f101112131415161718191a1b1c1d1e1f',
+ '676bb20380b0e301e8ab79590a396da78b834934',
+ 'f53aa2e9107a8b6c022c',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '0000000d0c0b0aa0a1a2a3a4a5'),
+ ( '000102030405060708090a0b',
+ '0c0d0e0f101112131415161718191a1b1c1d1e1f20',
+ 'c0ffa0d6f05bdb67f24d43a4338d2aa4bed7b20e43',
+ 'cd1aa31662e7ad65d6db',
+ 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf',
+ '0000000e0d0c0ba0a1a2a3a4a5'),
+ ( '0be1a88bace018b1',
+ '08e8cf97d820ea258460e96ad9cf5289054d895ceac47c',
+ '4cb97f86a2a4689a877947ab8091ef5386a6ffbdd080f8',
+ 'e78cf7cb0cddd7b3',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '00412b4ea9cdbe3c9696766cfa'),
+ ( '63018f76dc8a1bcb',
+ '9020ea6f91bdd85afa0039ba4baff9bfb79c7028949cd0ec',
+ '4ccb1e7ca981befaa0726c55d378061298c85c92814abc33',
+ 'c52ee81d7d77c08a',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '0033568ef7b2633c9696766cfa'),
+ ( 'aa6cfa36cae86b40',
+ 'b916e0eacc1c00d7dcec68ec0b3bbb1a02de8a2d1aa346132e',
+ 'b1d23a2220ddc0ac900d9aa03c61fcf4a559a4417767089708',
+ 'a776796edb723506',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '00103fe41336713c9696766cfa'),
+ ( 'd0d0735c531e1becf049c244',
+ '12daac5630efa5396f770ce1a66b21f7b2101c',
+ '14d253c3967b70609b7cbb7c49916028324526',
+ '9a6f49975bcadeaf',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '00764c63b8058e3c9696766cfa'),
+ ( '77b60f011c03e1525899bcae',
+ 'e88b6a46c78d63e52eb8c546efb5de6f75e9cc0d',
+ '5545ff1a085ee2efbf52b2e04bee1e2336c73e3f',
+ '762c0c7744fe7e3c',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '00f8b678094e3b3c9696766cfa'),
+ ( 'cd9044d2b71fdb8120ea60c0',
+ '6435acbafb11a82e2f071d7ca4a5ebd93a803ba87f',
+ '009769ecabdf48625594c59251e6035722675e04c8',
+ '47099e5ae0704551',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '00d560912d3f703c9696766cfa'),
+ ( 'd85bc7e69f944fb8',
+ '8a19b950bcf71a018e5e6701c91787659809d67dbedd18',
+ 'bc218daa947427b6db386a99ac1aef23ade0b52939cb6a',
+ '637cf9bec2408897c6ba',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '0042fff8f1951c3c9696766cfa'),
+ ( '74a0ebc9069f5b37',
+ '1761433c37c5a35fc1f39f406302eb907c6163be38c98437',
+ '5810e6fd25874022e80361a478e3e9cf484ab04f447efff6',
+ 'f0a477cc2fc9bf548944',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '00920f40e56cdc3c9696766cfa'),
+ ( '44a3aa3aae6475ca',
+ 'a434a8e58500c6e41530538862d686ea9e81301b5ae4226bfa',
+ 'f2beed7bc5098e83feb5b31608f8e29c38819a89c8e776f154',
+ '4d4151a4ed3a8b87b9ce',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '0027ca0c7120bc3c9696766cfa'),
+ ( 'ec46bb63b02520c33c49fd70',
+ 'b96b49e21d621741632875db7f6c9243d2d7c2',
+ '31d750a09da3ed7fddd49a2032aabf17ec8ebf',
+ '7d22c8088c666be5c197',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '005b8ccbcd9af83c9696766cfa'),
+ ( '47a65ac78b3d594227e85e71',
+ 'e2fcfbb880442c731bf95167c8ffd7895e337076',
+ 'e882f1dbd38ce3eda7c23f04dd65071eb41342ac',
+ 'df7e00dccec7ae52987d',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '003ebe94044b9a3c9696766cfa'),
+ ( '6e37a6ef546d955d34ab6059',
+ 'abf21c0b02feb88f856df4a37381bce3cc128517d4',
+ 'f32905b88a641b04b9c9ffb58cc390900f3da12ab1',
+ '6dce9e82efa16da62059',
+ 'd7828d13b2b0bdc325a76236df93cc6b',
+ '008d493b30ae8b3c9696766cfa'),
+ ]
+
+ test_vectors = [[unhexlify(x) for x in tv] for tv in test_vectors_hex]
+
+ def runTest(self):
+ for assoc_data, pt, ct, mac, key, nonce in self.test_vectors:
+ # Encrypt
+ cipher = AES.new(key, AES.MODE_CCM, nonce, mac_len=len(mac))
+ cipher.update(assoc_data)
+ ct2, mac2 = cipher.encrypt_and_digest(pt)
+ self.assertEqual(ct, ct2)
+ self.assertEqual(mac, mac2)
+
+ # Decrypt
+ cipher = AES.new(key, AES.MODE_CCM, nonce, mac_len=len(mac))
+ cipher.update(assoc_data)
+ pt2 = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(pt, pt2)
+
+
+class TestVectorsWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings, **extra_params):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._extra_params = extra_params
+ self._id = "None"
+
+ def setUp(self):
+
+ def filter_tag(group):
+ return group['tagSize'] // 8
+
+ self.tv = load_test_vectors_wycheproof(("Cipher", "wycheproof"),
+ "aes_ccm_test.json",
+ "Wycheproof AES CCM",
+ group_tag={'tag_size': filter_tag})
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_encrypt(self, tv):
+ self._id = "Wycheproof Encrypt CCM Test #" + str(tv.id)
+
+ try:
+ cipher = AES.new(tv.key, AES.MODE_CCM, tv.iv, mac_len=tv.tag_size,
+ **self._extra_params)
+ except ValueError as e:
+ if len(tv.iv) not in range(7, 13 + 1, 2) and "Length of parameter 'nonce'" in str(e):
+ assert not tv.valid
+ return
+ if tv.tag_size not in range(4, 16 + 1, 2) and "Parameter 'mac_len'" in str(e):
+ assert not tv.valid
+ return
+ raise e
+
+ cipher.update(tv.aad)
+ ct, tag = cipher.encrypt_and_digest(tv.msg)
+ if tv.valid:
+ self.assertEqual(ct, tv.ct)
+ self.assertEqual(tag, tv.tag)
+ self.warn(tv)
+
+ def test_decrypt(self, tv):
+ self._id = "Wycheproof Decrypt CCM Test #" + str(tv.id)
+
+ try:
+ cipher = AES.new(tv.key, AES.MODE_CCM, tv.iv, mac_len=tv.tag_size,
+ **self._extra_params)
+ except ValueError as e:
+ if len(tv.iv) not in range(7, 13 + 1, 2) and "Length of parameter 'nonce'" in str(e):
+ assert not tv.valid
+ return
+ if tv.tag_size not in range(4, 16 + 1, 2) and "Parameter 'mac_len'" in str(e):
+ assert not tv.valid
+ return
+ raise e
+
+ cipher.update(tv.aad)
+ try:
+ pt = cipher.decrypt_and_verify(tv.ct, tv.tag)
+ except ValueError:
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.assertEqual(pt, tv.msg)
+ self.warn(tv)
+
+ def test_corrupt_decrypt(self, tv):
+ self._id = "Wycheproof Corrupt Decrypt CCM Test #" + str(tv.id)
+ if len(tv.iv) not in range(7, 13 + 1, 2) or len(tv.ct) == 0:
+ return
+ cipher = AES.new(tv.key, AES.MODE_CCM, tv.iv, mac_len=tv.tag_size,
+ **self._extra_params)
+ cipher.update(tv.aad)
+ ct_corrupt = strxor(tv.ct, b"\x00" * (len(tv.ct) - 1) + b"\x01")
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct_corrupt, tv.tag)
+
+ def runTest(self):
+
+ for tv in self.tv:
+ self.test_encrypt(tv)
+ self.test_decrypt(tv)
+ self.test_corrupt_decrypt(tv)
+
+
+def get_tests(config={}):
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ tests = []
+ tests += list_test_cases(CcmTests)
+ tests += list_test_cases(CcmFSMTests)
+ tests += [TestVectors()]
+ tests += [TestVectorsWycheproof(wycheproof_warnings)]
+
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CFB.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CFB.py
new file mode 100644
index 00000000..cb0c3529
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CFB.py
@@ -0,0 +1,411 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Util.py3compat import tobytes, is_string
+from Crypto.Cipher import AES, DES3, DES
+from Crypto.Hash import SHAKE128
+
+from Crypto.SelfTest.Cipher.test_CBC import BlockChainingTests
+
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+
+class CfbTests(BlockChainingTests):
+
+ aes_mode = AES.MODE_CFB
+ des3_mode = DES3.MODE_CFB
+
+ # Redefine test_unaligned_data_128/64
+
+ def test_unaligned_data_128(self):
+ plaintexts = [ b"7777777" ] * 100
+
+ cipher = AES.new(self.key_128, AES.MODE_CFB, self.iv_128, segment_size=8)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = AES.new(self.key_128, AES.MODE_CFB, self.iv_128, segment_size=8)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ cipher = AES.new(self.key_128, AES.MODE_CFB, self.iv_128, segment_size=128)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = AES.new(self.key_128, AES.MODE_CFB, self.iv_128, segment_size=128)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ def test_unaligned_data_64(self):
+ plaintexts = [ b"7777777" ] * 100
+ cipher = DES3.new(self.key_192, DES3.MODE_CFB, self.iv_64, segment_size=8)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = DES3.new(self.key_192, DES3.MODE_CFB, self.iv_64, segment_size=8)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ cipher = DES3.new(self.key_192, DES3.MODE_CFB, self.iv_64, segment_size=64)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = DES3.new(self.key_192, DES3.MODE_CFB, self.iv_64, segment_size=64)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ # Extra
+
+ def test_segment_size_128(self):
+ for bits in range(8, 129, 8):
+ cipher = AES.new(self.key_128, AES.MODE_CFB, self.iv_128,
+ segment_size=bits)
+
+ for bits in 0, 7, 9, 127, 129:
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CFB,
+ self.iv_128,
+ segment_size=bits)
+
+ def test_segment_size_64(self):
+ for bits in range(8, 65, 8):
+ cipher = DES3.new(self.key_192, DES3.MODE_CFB, self.iv_64,
+ segment_size=bits)
+
+ for bits in 0, 7, 9, 63, 65:
+ self.assertRaises(ValueError, DES3.new, self.key_192, AES.MODE_CFB,
+ self.iv_64,
+ segment_size=bits)
+
+
+class NistCfbVectors(unittest.TestCase):
+
+ def _do_kat_aes_test(self, file_name, segment_size):
+
+ test_vectors = load_test_vectors(("Cipher", "AES"),
+ file_name,
+ "AES CFB%d KAT" % segment_size,
+ { "count" : lambda x: int(x) } )
+ if test_vectors is None:
+ return
+
+ direction = None
+ for tv in test_vectors:
+
+ # The test vector file contains some directive lines
+ if is_string(tv):
+ direction = tv
+ continue
+
+ self.description = tv.desc
+ cipher = AES.new(tv.key, AES.MODE_CFB, tv.iv,
+ segment_size=segment_size)
+ if direction == "[ENCRYPT]":
+ self.assertEqual(cipher.encrypt(tv.plaintext), tv.ciphertext)
+ elif direction == "[DECRYPT]":
+ self.assertEqual(cipher.decrypt(tv.ciphertext), tv.plaintext)
+ else:
+ assert False
+
+ # See Section 6.4.5 in AESAVS
+ def _do_mct_aes_test(self, file_name, segment_size):
+
+ test_vectors = load_test_vectors(("Cipher", "AES"),
+ file_name,
+ "AES CFB%d Montecarlo" % segment_size,
+ { "count" : lambda x: int(x) } )
+ if test_vectors is None:
+ return
+
+ assert(segment_size in (8, 128))
+
+ direction = None
+ for tv in test_vectors:
+
+ # The test vector file contains some directive lines
+ if is_string(tv):
+ direction = tv
+ continue
+
+ self.description = tv.desc
+ cipher = AES.new(tv.key, AES.MODE_CFB, tv.iv,
+ segment_size=segment_size)
+
+ def get_input(input_text, output_seq, j):
+ # CFB128
+ if segment_size == 128:
+ if j >= 2:
+ return output_seq[-2]
+ return [input_text, tv.iv][j]
+ # CFB8
+ if j == 0:
+ return input_text
+ elif j <= 16:
+ return tv.iv[j - 1:j]
+ return output_seq[j - 17]
+
+ if direction == '[ENCRYPT]':
+ cts = []
+ for j in range(1000):
+ plaintext = get_input(tv.plaintext, cts, j)
+ cts.append(cipher.encrypt(plaintext))
+ self.assertEqual(cts[-1], tv.ciphertext)
+ elif direction == '[DECRYPT]':
+ pts = []
+ for j in range(1000):
+ ciphertext = get_input(tv.ciphertext, pts, j)
+ pts.append(cipher.decrypt(ciphertext))
+ self.assertEqual(pts[-1], tv.plaintext)
+ else:
+ assert False
+
+ def _do_tdes_test(self, file_name, segment_size):
+
+ test_vectors = load_test_vectors(("Cipher", "TDES"),
+ file_name,
+ "TDES CFB%d KAT" % segment_size,
+ { "count" : lambda x: int(x) } )
+ if test_vectors is None:
+ return
+
+ direction = None
+ for tv in test_vectors:
+
+ # The test vector file contains some directive lines
+ if is_string(tv):
+ direction = tv
+ continue
+
+ self.description = tv.desc
+ if hasattr(tv, "keys"):
+ cipher = DES.new(tv.keys, DES.MODE_CFB, tv.iv,
+ segment_size=segment_size)
+ else:
+ if tv.key1 != tv.key3:
+ key = tv.key1 + tv.key2 + tv.key3 # Option 3
+ else:
+ key = tv.key1 + tv.key2 # Option 2
+ cipher = DES3.new(key, DES3.MODE_CFB, tv.iv,
+ segment_size=segment_size)
+ if direction == "[ENCRYPT]":
+ self.assertEqual(cipher.encrypt(tv.plaintext), tv.ciphertext)
+ elif direction == "[DECRYPT]":
+ self.assertEqual(cipher.decrypt(tv.ciphertext), tv.plaintext)
+ else:
+ assert False
+
+
+# Create one test method per file
+nist_aes_kat_mmt_files = (
+ # KAT
+ "CFB?GFSbox128.rsp",
+ "CFB?GFSbox192.rsp",
+ "CFB?GFSbox256.rsp",
+ "CFB?KeySbox128.rsp",
+ "CFB?KeySbox192.rsp",
+ "CFB?KeySbox256.rsp",
+ "CFB?VarKey128.rsp",
+ "CFB?VarKey192.rsp",
+ "CFB?VarKey256.rsp",
+ "CFB?VarTxt128.rsp",
+ "CFB?VarTxt192.rsp",
+ "CFB?VarTxt256.rsp",
+ # MMT
+ "CFB?MMT128.rsp",
+ "CFB?MMT192.rsp",
+ "CFB?MMT256.rsp",
+ )
+nist_aes_mct_files = (
+ "CFB?MCT128.rsp",
+ "CFB?MCT192.rsp",
+ "CFB?MCT256.rsp",
+ )
+
+for file_gen_name in nist_aes_kat_mmt_files:
+ for bits in "8", "128":
+ file_name = file_gen_name.replace("?", bits)
+ def new_func(self, file_name=file_name, bits=bits):
+ self._do_kat_aes_test(file_name, int(bits))
+ setattr(NistCfbVectors, "test_AES_" + file_name, new_func)
+
+for file_gen_name in nist_aes_mct_files:
+ for bits in "8", "128":
+ file_name = file_gen_name.replace("?", bits)
+ def new_func(self, file_name=file_name, bits=bits):
+ self._do_mct_aes_test(file_name, int(bits))
+ setattr(NistCfbVectors, "test_AES_" + file_name, new_func)
+del file_name, new_func
+
+nist_tdes_files = (
+ "TCFB?MMT2.rsp", # 2TDES
+ "TCFB?MMT3.rsp", # 3TDES
+ "TCFB?invperm.rsp", # Single DES
+ "TCFB?permop.rsp",
+ "TCFB?subtab.rsp",
+ "TCFB?varkey.rsp",
+ "TCFB?vartext.rsp",
+ )
+
+for file_gen_name in nist_tdes_files:
+ for bits in "8", "64":
+ file_name = file_gen_name.replace("?", bits)
+ def new_func(self, file_name=file_name, bits=bits):
+ self._do_tdes_test(file_name, int(bits))
+ setattr(NistCfbVectors, "test_TDES_" + file_name, new_func)
+
+# END OF NIST CBC TEST VECTORS
+
+
+class SP800TestVectors(unittest.TestCase):
+ """Class exercising the CFB test vectors found in Section F.3
+ of NIST SP 800-3A"""
+
+ def test_aes_128_cfb8(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172aae2d'
+ ciphertext = '3b79424c9c0dd436bace9e0ed4586a4f32b9'
+ key = '2b7e151628aed2a6abf7158809cf4f3c'
+ iv = '000102030405060708090a0b0c0d0e0f'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=8)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=8)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ def test_aes_192_cfb8(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172aae2d'
+ ciphertext = 'cda2521ef0a905ca44cd057cbf0d47a0678a'
+ key = '8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b'
+ iv = '000102030405060708090a0b0c0d0e0f'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=8)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=8)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ def test_aes_256_cfb8(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172aae2d'
+ ciphertext = 'dc1f1a8520a64db55fcc8ac554844e889700'
+ key = '603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4'
+ iv = '000102030405060708090a0b0c0d0e0f'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=8)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=8)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ def test_aes_128_cfb128(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = '3b3fd92eb72dad20333449f8e83cfb4a' +\
+ 'c8a64537a0b3a93fcde3cdad9f1ce58b' +\
+ '26751f67a3cbb140b1808cf187a4f4df' +\
+ 'c04b05357c5d1c0eeac4c66f9ff7f2e6'
+ key = '2b7e151628aed2a6abf7158809cf4f3c'
+ iv = '000102030405060708090a0b0c0d0e0f'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ def test_aes_192_cfb128(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = 'cdc80d6fddf18cab34c25909c99a4174' +\
+ '67ce7f7f81173621961a2b70171d3d7a' +\
+ '2e1e8a1dd59b88b1c8e60fed1efac4c9' +\
+ 'c05f9f9ca9834fa042ae8fba584b09ff'
+ key = '8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b'
+ iv = '000102030405060708090a0b0c0d0e0f'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ def test_aes_256_cfb128(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+
+ ciphertext = 'dc7e84bfda79164b7ecd8486985d3860' +\
+ '39ffed143b28b1c832113c6331e5407b' +\
+ 'df10132415e54b92a13ed0a8267ae2f9' +\
+ '75a385741ab9cef82031623d55b1e471'
+ key = '603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4'
+ iv = '000102030405060708090a0b0c0d0e0f'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(CfbTests)
+ if config.get('slow_tests'):
+ tests += list_test_cases(NistCfbVectors)
+ tests += list_test_cases(SP800TestVectors)
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CTR.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CTR.py
new file mode 100644
index 00000000..6fc43ef7
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_CTR.py
@@ -0,0 +1,472 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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 unittest
+from binascii import hexlify, unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Util.py3compat import tobytes, bchr
+from Crypto.Cipher import AES, DES3
+from Crypto.Hash import SHAKE128, SHA256
+from Crypto.Util import Counter
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+class CtrTests(unittest.TestCase):
+
+ key_128 = get_tag_random("key_128", 16)
+ key_192 = get_tag_random("key_192", 24)
+ nonce_32 = get_tag_random("nonce_32", 4)
+ nonce_64 = get_tag_random("nonce_64", 8)
+ ctr_64 = Counter.new(32, prefix=nonce_32)
+ ctr_128 = Counter.new(64, prefix=nonce_64)
+
+ def test_loopback_128(self):
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ pt = get_tag_random("plaintext", 16 * 100)
+ ct = cipher.encrypt(pt)
+
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_loopback_64(self):
+ cipher = DES3.new(self.key_192, DES3.MODE_CTR, counter=self.ctr_64)
+ pt = get_tag_random("plaintext", 8 * 100)
+ ct = cipher.encrypt(pt)
+
+ cipher = DES3.new(self.key_192, DES3.MODE_CTR, counter=self.ctr_64)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_invalid_counter_parameter(self):
+ # Counter object is required for ciphers with short block size
+ self.assertRaises(TypeError, DES3.new, self.key_192, AES.MODE_CTR)
+ # Positional arguments are not allowed (Counter must be passed as
+ # keyword)
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_CTR, self.ctr_128)
+
+ def test_nonce_attribute(self):
+ # Nonce attribute is the prefix passed to Counter (DES3)
+ cipher = DES3.new(self.key_192, DES3.MODE_CTR, counter=self.ctr_64)
+ self.assertEqual(cipher.nonce, self.nonce_32)
+
+ # Nonce attribute is the prefix passed to Counter (AES)
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ self.assertEqual(cipher.nonce, self.nonce_64)
+
+ # Nonce attribute is not defined if suffix is used in Counter
+ counter = Counter.new(64, prefix=self.nonce_32, suffix=self.nonce_32)
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=counter)
+ self.assertFalse(hasattr(cipher, "nonce"))
+
+ def test_nonce_parameter(self):
+ # Nonce parameter becomes nonce attribute
+ cipher1 = AES.new(self.key_128, AES.MODE_CTR, nonce=self.nonce_64)
+ self.assertEqual(cipher1.nonce, self.nonce_64)
+
+ counter = Counter.new(64, prefix=self.nonce_64, initial_value=0)
+ cipher2 = AES.new(self.key_128, AES.MODE_CTR, counter=counter)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ pt = get_tag_random("plaintext", 65536)
+ self.assertEqual(cipher1.encrypt(pt), cipher2.encrypt(pt))
+
+ # Nonce is implicitly created (for AES) when no parameters are passed
+ nonce1 = AES.new(self.key_128, AES.MODE_CTR).nonce
+ nonce2 = AES.new(self.key_128, AES.MODE_CTR).nonce
+ self.assertNotEqual(nonce1, nonce2)
+ self.assertEqual(len(nonce1), 8)
+
+ # Nonce can be zero-length
+ cipher = AES.new(self.key_128, AES.MODE_CTR, nonce=b"")
+ self.assertEqual(b"", cipher.nonce)
+ cipher.encrypt(b'0'*300)
+
+ # Nonce and Counter are mutually exclusive
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_CTR,
+ counter=self.ctr_128, nonce=self.nonce_64)
+
+ def test_initial_value_parameter(self):
+ # Test with nonce parameter
+ cipher1 = AES.new(self.key_128, AES.MODE_CTR,
+ nonce=self.nonce_64, initial_value=0xFFFF)
+ counter = Counter.new(64, prefix=self.nonce_64, initial_value=0xFFFF)
+ cipher2 = AES.new(self.key_128, AES.MODE_CTR, counter=counter)
+ pt = get_tag_random("plaintext", 65536)
+ self.assertEqual(cipher1.encrypt(pt), cipher2.encrypt(pt))
+
+ # Test without nonce parameter
+ cipher1 = AES.new(self.key_128, AES.MODE_CTR,
+ initial_value=0xFFFF)
+ counter = Counter.new(64, prefix=cipher1.nonce, initial_value=0xFFFF)
+ cipher2 = AES.new(self.key_128, AES.MODE_CTR, counter=counter)
+ pt = get_tag_random("plaintext", 65536)
+ self.assertEqual(cipher1.encrypt(pt), cipher2.encrypt(pt))
+
+ # Initial_value and Counter are mutually exclusive
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_CTR,
+ counter=self.ctr_128, initial_value=0)
+
+ def test_initial_value_bytes_parameter(self):
+ # Same result as when passing an integer
+ cipher1 = AES.new(self.key_128, AES.MODE_CTR,
+ nonce=self.nonce_64,
+ initial_value=b"\x00"*6+b"\xFF\xFF")
+ cipher2 = AES.new(self.key_128, AES.MODE_CTR,
+ nonce=self.nonce_64, initial_value=0xFFFF)
+ pt = get_tag_random("plaintext", 65536)
+ self.assertEqual(cipher1.encrypt(pt), cipher2.encrypt(pt))
+
+ # Fail if the iv is too large
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CTR,
+ initial_value=b"5"*17)
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CTR,
+ nonce=self.nonce_64, initial_value=b"5"*9)
+
+ # Fail if the iv is too short
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CTR,
+ initial_value=b"5"*15)
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CTR,
+ nonce=self.nonce_64, initial_value=b"5"*7)
+
+ def test_iv_with_matching_length(self):
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CTR,
+ counter=Counter.new(120))
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_CTR,
+ counter=Counter.new(136))
+
+ def test_block_size_128(self):
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ self.assertEqual(cipher.block_size, AES.block_size)
+
+ def test_block_size_64(self):
+ cipher = DES3.new(self.key_192, DES3.MODE_CTR, counter=self.ctr_64)
+ self.assertEqual(cipher.block_size, DES3.block_size)
+
+ def test_unaligned_data_128(self):
+ plaintexts = [ b"7777777" ] * 100
+
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ def test_unaligned_data_64(self):
+ plaintexts = [ b"7777777" ] * 100
+ cipher = DES3.new(self.key_192, AES.MODE_CTR, counter=self.ctr_64)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = DES3.new(self.key_192, AES.MODE_CTR, counter=self.ctr_64)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ cipher = DES3.new(self.key_192, AES.MODE_CTR, counter=self.ctr_64)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = DES3.new(self.key_192, AES.MODE_CTR, counter=self.ctr_64)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ def test_unknown_parameters(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_CTR,
+ 7, counter=self.ctr_128)
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_CTR,
+ counter=self.ctr_128, unknown=7)
+ # But some are only known by the base cipher (e.g. use_aesni consumed by the AES module)
+ AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128, use_aesni=False)
+
+ def test_null_encryption_decryption(self):
+ for func in "encrypt", "decrypt":
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ result = getattr(cipher, func)(b"")
+ self.assertEqual(result, b"")
+
+ def test_either_encrypt_or_decrypt(self):
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ cipher.encrypt(b"")
+ self.assertRaises(TypeError, cipher.decrypt, b"")
+
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=self.ctr_128)
+ cipher.decrypt(b"")
+ self.assertRaises(TypeError, cipher.encrypt, b"")
+
+ def test_wrap_around(self):
+ # Counter is only 8 bits, so we can only encrypt/decrypt 256 blocks (=4096 bytes)
+ counter = Counter.new(8, prefix=bchr(9) * 15)
+ max_bytes = 4096
+
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=counter)
+ cipher.encrypt(b'9' * max_bytes)
+ self.assertRaises(OverflowError, cipher.encrypt, b'9')
+
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=counter)
+ self.assertRaises(OverflowError, cipher.encrypt, b'9' * (max_bytes + 1))
+
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=counter)
+ cipher.decrypt(b'9' * max_bytes)
+ self.assertRaises(OverflowError, cipher.decrypt, b'9')
+
+ cipher = AES.new(self.key_128, AES.MODE_CTR, counter=counter)
+ self.assertRaises(OverflowError, cipher.decrypt, b'9' * (max_bytes + 1))
+
+ def test_bytearray(self):
+ data = b"1" * 16
+ iv = b"\x00" * 6 + b"\xFF\xFF"
+
+ # Encrypt
+ cipher1 = AES.new(self.key_128, AES.MODE_CTR,
+ nonce=self.nonce_64,
+ initial_value=iv)
+ ref1 = cipher1.encrypt(data)
+
+ cipher2 = AES.new(self.key_128, AES.MODE_CTR,
+ nonce=bytearray(self.nonce_64),
+ initial_value=bytearray(iv))
+ ref2 = cipher2.encrypt(bytearray(data))
+
+ self.assertEqual(ref1, ref2)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ cipher3 = AES.new(self.key_128, AES.MODE_CTR,
+ nonce=self.nonce_64,
+ initial_value=iv)
+ ref3 = cipher3.decrypt(data)
+
+ cipher4 = AES.new(self.key_128, AES.MODE_CTR,
+ nonce=bytearray(self.nonce_64),
+ initial_value=bytearray(iv))
+ ref4 = cipher4.decrypt(bytearray(data))
+
+ self.assertEqual(ref3, ref4)
+
+ def test_very_long_data(self):
+ cipher = AES.new(b'A' * 32, AES.MODE_CTR, nonce=b'')
+ ct = cipher.encrypt(b'B' * 1000000)
+ digest = SHA256.new(ct).hexdigest()
+ self.assertEqual(digest, "96204fc470476561a3a8f3b6fe6d24be85c87510b638142d1d0fb90989f8a6a6")
+
+ def test_output_param(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(128)
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ def test_output_param_memoryview(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ ct = cipher.encrypt(pt)
+
+ output = memoryview(bytearray(128))
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ def test_output_param_neg(self):
+ LEN_PT = 128
+
+ pt = b'5' * LEN_PT
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ ct = cipher.encrypt(pt)
+
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0' * LEN_PT)
+
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0' * LEN_PT)
+
+ shorter_output = bytearray(LEN_PT - 1)
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ cipher = AES.new(b'4'*16, AES.MODE_CTR, nonce=self.nonce_64)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+class SP800TestVectors(unittest.TestCase):
+ """Class exercising the CTR test vectors found in Section F.5
+ of NIST SP 800-38A"""
+
+ def test_aes_128(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = '874d6191b620e3261bef6864990db6ce' +\
+ '9806f66b7970fdff8617187bb9fffdff' +\
+ '5ae4df3edbd5d35e5b4f09020db03eab' +\
+ '1e031dda2fbe03d1792170a0f3009cee'
+ key = '2b7e151628aed2a6abf7158809cf4f3c'
+ counter = Counter.new(nbits=16,
+ prefix=unhexlify('f0f1f2f3f4f5f6f7f8f9fafbfcfd'),
+ initial_value=0xfeff)
+
+ key = unhexlify(key)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CTR, counter=counter)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CTR, counter=counter)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ def test_aes_192(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = '1abc932417521ca24f2b0459fe7e6e0b' +\
+ '090339ec0aa6faefd5ccc2c6f4ce8e94' +\
+ '1e36b26bd1ebc670d1bd1d665620abf7' +\
+ '4f78a7f6d29809585a97daec58c6b050'
+ key = '8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b'
+ counter = Counter.new(nbits=16,
+ prefix=unhexlify('f0f1f2f3f4f5f6f7f8f9fafbfcfd'),
+ initial_value=0xfeff)
+
+ key = unhexlify(key)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CTR, counter=counter)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CTR, counter=counter)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ def test_aes_256(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = '601ec313775789a5b7a7f504bbf3d228' +\
+ 'f443e3ca4d62b59aca84e990cacaf5c5' +\
+ '2b0930daa23de94ce87017ba2d84988d' +\
+ 'dfc9c58db67aada613c2dd08457941a6'
+ key = '603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4'
+ counter = Counter.new(nbits=16,
+ prefix=unhexlify('f0f1f2f3f4f5f6f7f8f9fafbfcfd'),
+ initial_value=0xfeff)
+ key = unhexlify(key)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_CTR, counter=counter)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_CTR, counter=counter)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+
+class RFC3686TestVectors(unittest.TestCase):
+
+ # Each item is a test vector with:
+ # - plaintext
+ # - ciphertext
+ # - key (AES 128, 192 or 256 bits)
+ # - counter prefix (4 byte nonce + 8 byte nonce)
+ data = (
+ ('53696e676c6520626c6f636b206d7367',
+ 'e4095d4fb7a7b3792d6175a3261311b8',
+ 'ae6852f8121067cc4bf7a5765577f39e',
+ '000000300000000000000000'),
+ ('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
+ '5104a106168a72d9790d41ee8edad388eb2e1efc46da57c8fce630df9141be28',
+ '7e24067817fae0d743d6ce1f32539163',
+ '006cb6dbc0543b59da48d90b'),
+ ('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223',
+ 'c1cf48a89f2ffdd9cf4652e9efdb72d74540a42bde6d7836d59a5ceaaef3105325b2072f',
+ '7691be035e5020a8ac6e618529f9a0dc',
+ '00e0017b27777f3f4a1786f0'),
+ ('53696e676c6520626c6f636b206d7367',
+ '4b55384fe259c9c84e7935a003cbe928',
+ '16af5b145fc9f579c175f93e3bfb0eed863d06ccfdb78515',
+ '0000004836733c147d6d93cb'),
+ ('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
+ '453243fc609b23327edfaafa7131cd9f8490701c5ad4a79cfc1fe0ff42f4fb00',
+ '7c5cb2401b3dc33c19e7340819e0f69c678c3db8e6f6a91a',
+ '0096b03b020c6eadc2cb500d'),
+ ('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223',
+ '96893fc55e5c722f540b7dd1ddf7e758d288bc95c69165884536c811662f2188abee0935',
+ '02bf391ee8ecb159b959617b0965279bf59b60a786d3e0fe',
+ '0007bdfd5cbd60278dcc0912'),
+ ('53696e676c6520626c6f636b206d7367',
+ '145ad01dbf824ec7560863dc71e3e0c0',
+ '776beff2851db06f4c8a0542c8696f6c6a81af1eec96b4d37fc1d689e6c1c104',
+ '00000060db5672c97aa8f0b2'),
+ ('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
+ 'f05e231b3894612c49ee000b804eb2a9b8306b508f839d6a5530831d9344af1c',
+ 'f6d66d6bd52d59bb0796365879eff886c66dd51a5b6a99744b50590c87a23884',
+ '00faac24c1585ef15a43d875'),
+ ('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223',
+ 'eb6c52821d0bbbf7ce7594462aca4faab407df866569fd07f48cc0b583d6071f1ec0e6b8',
+ 'ff7a617ce69148e4f1726e2f43581de2aa62d9f805532edff1eed687fb54153d',
+ '001cc5b751a51d70a1c11148')
+ )
+
+ bindata = []
+ for tv in data:
+ bindata.append([unhexlify(x) for x in tv])
+
+ def runTest(self):
+ for pt, ct, key, prefix in self.bindata:
+ counter = Counter.new(32, prefix=prefix)
+ cipher = AES.new(key, AES.MODE_CTR, counter=counter)
+ result = cipher.encrypt(pt)
+ self.assertEqual(hexlify(ct), hexlify(result))
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(CtrTests)
+ tests += list_test_cases(SP800TestVectors)
+ tests += [ RFC3686TestVectors() ]
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ChaCha20.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ChaCha20.py
new file mode 100644
index 00000000..4396ac28
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ChaCha20.py
@@ -0,0 +1,529 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 os
+import re
+import unittest
+from binascii import hexlify, unhexlify
+
+from Crypto.Util.py3compat import b, tobytes, bchr
+from Crypto.Util.strxor import strxor_c
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Cipher import ChaCha20
+
+
+class ChaCha20Test(unittest.TestCase):
+
+ def test_new_positive(self):
+ cipher = ChaCha20.new(key=b("0")*32, nonce=b"0"*8)
+ self.assertEqual(cipher.nonce, b"0" * 8)
+ cipher = ChaCha20.new(key=b("0")*32, nonce=b"0"*12)
+ self.assertEqual(cipher.nonce, b"0" * 12)
+
+ def test_new_negative(self):
+ new = ChaCha20.new
+ self.assertRaises(TypeError, new)
+ self.assertRaises(TypeError, new, nonce=b("0"))
+ self.assertRaises(ValueError, new, nonce=b("0")*8, key=b("0"))
+ self.assertRaises(ValueError, new, nonce=b("0"), key=b("0")*32)
+
+ def test_default_nonce(self):
+ cipher1 = ChaCha20.new(key=bchr(1) * 32)
+ cipher2 = ChaCha20.new(key=bchr(1) * 32)
+ self.assertEqual(len(cipher1.nonce), 8)
+ self.assertNotEqual(cipher1.nonce, cipher2.nonce)
+
+ def test_nonce(self):
+ key = b'A' * 32
+
+ nonce1 = b'P' * 8
+ cipher1 = ChaCha20.new(key=key, nonce=nonce1)
+ self.assertEqual(nonce1, cipher1.nonce)
+
+ nonce2 = b'Q' * 12
+ cipher2 = ChaCha20.new(key=key, nonce=nonce2)
+ self.assertEqual(nonce2, cipher2.nonce)
+
+ def test_eiter_encrypt_or_decrypt(self):
+ """Verify that a cipher cannot be used for both decrypting and encrypting"""
+
+ c1 = ChaCha20.new(key=b("5") * 32, nonce=b("6") * 8)
+ c1.encrypt(b("8"))
+ self.assertRaises(TypeError, c1.decrypt, b("9"))
+
+ c2 = ChaCha20.new(key=b("5") * 32, nonce=b("6") * 8)
+ c2.decrypt(b("8"))
+ self.assertRaises(TypeError, c2.encrypt, b("9"))
+
+ def test_round_trip(self):
+ pt = b("A") * 1024
+ c1 = ChaCha20.new(key=b("5") * 32, nonce=b("6") * 8)
+ c2 = ChaCha20.new(key=b("5") * 32, nonce=b("6") * 8)
+ ct = c1.encrypt(pt)
+ self.assertEqual(c2.decrypt(ct), pt)
+
+ self.assertEqual(c1.encrypt(b("")), b(""))
+ self.assertEqual(c2.decrypt(b("")), b(""))
+
+ def test_streaming(self):
+ """Verify that an arbitrary number of bytes can be encrypted/decrypted"""
+ from Crypto.Hash import SHA1
+
+ segments = (1, 3, 5, 7, 11, 17, 23)
+ total = sum(segments)
+
+ pt = b("")
+ while len(pt) < total:
+ pt += SHA1.new(pt).digest()
+
+ cipher1 = ChaCha20.new(key=b("7") * 32, nonce=b("t") * 8)
+ ct = cipher1.encrypt(pt)
+
+ cipher2 = ChaCha20.new(key=b("7") * 32, nonce=b("t") * 8)
+ cipher3 = ChaCha20.new(key=b("7") * 32, nonce=b("t") * 8)
+ idx = 0
+ for segment in segments:
+ self.assertEqual(cipher2.decrypt(ct[idx:idx+segment]), pt[idx:idx+segment])
+ self.assertEqual(cipher3.encrypt(pt[idx:idx+segment]), ct[idx:idx+segment])
+ idx += segment
+
+ def test_seek(self):
+ cipher1 = ChaCha20.new(key=b("9") * 32, nonce=b("e") * 8)
+
+ offset = 64 * 900 + 7
+ pt = b("1") * 64
+
+ cipher1.encrypt(b("0") * offset)
+ ct1 = cipher1.encrypt(pt)
+
+ cipher2 = ChaCha20.new(key=b("9") * 32, nonce=b("e") * 8)
+ cipher2.seek(offset)
+ ct2 = cipher2.encrypt(pt)
+
+ self.assertEqual(ct1, ct2)
+
+ def test_seek_tv(self):
+ # Test Vector #4, A.1 from
+ # http://tools.ietf.org/html/draft-nir-cfrg-chacha20-poly1305-04
+ key = bchr(0) + bchr(255) + bchr(0) * 30
+ nonce = bchr(0) * 8
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ cipher.seek(64 * 2)
+ expected_key_stream = unhexlify(b(
+ "72d54dfbf12ec44b362692df94137f32"
+ "8fea8da73990265ec1bbbea1ae9af0ca"
+ "13b25aa26cb4a648cb9b9d1be65b2c09"
+ "24a66c54d545ec1b7374f4872e99f096"
+ ))
+ ct = cipher.encrypt(bchr(0) * len(expected_key_stream))
+ self.assertEqual(expected_key_stream, ct)
+
+ def test_rfc7539(self):
+ # from https://tools.ietf.org/html/rfc7539 Annex A.1
+ # Each item is: key, nonce, block #, plaintext, ciphertext
+ tvs = [
+ # Test Vector #1
+ (
+ "00"*32,
+ "00"*12,
+ 0,
+ "00"*16*4,
+ "76b8e0ada0f13d90405d6ae55386bd28"
+ "bdd219b8a08ded1aa836efcc8b770dc7"
+ "da41597c5157488d7724e03fb8d84a37"
+ "6a43b8f41518a11cc387b669b2ee6586"
+ ),
+ # Test Vector #2
+ (
+ "00"*31 + "01",
+ "00"*11 + "02",
+ 1,
+ "416e79207375626d697373696f6e2074"
+ "6f20746865204945544620696e74656e"
+ "6465642062792074686520436f6e7472"
+ "696275746f7220666f72207075626c69"
+ "636174696f6e20617320616c6c206f72"
+ "2070617274206f6620616e2049455446"
+ "20496e7465726e65742d447261667420"
+ "6f722052464320616e6420616e792073"
+ "746174656d656e74206d616465207769"
+ "7468696e2074686520636f6e74657874"
+ "206f6620616e20494554462061637469"
+ "7669747920697320636f6e7369646572"
+ "656420616e20224945544620436f6e74"
+ "7269627574696f6e222e205375636820"
+ "73746174656d656e747320696e636c75"
+ "6465206f72616c2073746174656d656e"
+ "747320696e2049455446207365737369"
+ "6f6e732c2061732077656c6c20617320"
+ "7772697474656e20616e6420656c6563"
+ "74726f6e696320636f6d6d756e696361"
+ "74696f6e73206d61646520617420616e"
+ "792074696d65206f7220706c6163652c"
+ "20776869636820617265206164647265"
+ "7373656420746f",
+ "a3fbf07df3fa2fde4f376ca23e827370"
+ "41605d9f4f4f57bd8cff2c1d4b7955ec"
+ "2a97948bd3722915c8f3d337f7d37005"
+ "0e9e96d647b7c39f56e031ca5eb6250d"
+ "4042e02785ececfa4b4bb5e8ead0440e"
+ "20b6e8db09d881a7c6132f420e527950"
+ "42bdfa7773d8a9051447b3291ce1411c"
+ "680465552aa6c405b7764d5e87bea85a"
+ "d00f8449ed8f72d0d662ab052691ca66"
+ "424bc86d2df80ea41f43abf937d3259d"
+ "c4b2d0dfb48a6c9139ddd7f76966e928"
+ "e635553ba76c5c879d7b35d49eb2e62b"
+ "0871cdac638939e25e8a1e0ef9d5280f"
+ "a8ca328b351c3c765989cbcf3daa8b6c"
+ "cc3aaf9f3979c92b3720fc88dc95ed84"
+ "a1be059c6499b9fda236e7e818b04b0b"
+ "c39c1e876b193bfe5569753f88128cc0"
+ "8aaa9b63d1a16f80ef2554d7189c411f"
+ "5869ca52c5b83fa36ff216b9c1d30062"
+ "bebcfd2dc5bce0911934fda79a86f6e6"
+ "98ced759c3ff9b6477338f3da4f9cd85"
+ "14ea9982ccafb341b2384dd902f3d1ab"
+ "7ac61dd29c6f21ba5b862f3730e37cfd"
+ "c4fd806c22f221"
+ ),
+ # Test Vector #3
+ (
+ "1c9240a5eb55d38af333888604f6b5f0"
+ "473917c1402b80099dca5cbc207075c0",
+ "00"*11 + "02",
+ 42,
+ "2754776173206272696c6c69672c2061"
+ "6e642074686520736c6974687920746f"
+ "7665730a446964206779726520616e64"
+ "2067696d626c6520696e207468652077"
+ "6162653a0a416c6c206d696d73792077"
+ "6572652074686520626f726f676f7665"
+ "732c0a416e6420746865206d6f6d6520"
+ "7261746873206f757467726162652e",
+ "62e6347f95ed87a45ffae7426f27a1df"
+ "5fb69110044c0d73118effa95b01e5cf"
+ "166d3df2d721caf9b21e5fb14c616871"
+ "fd84c54f9d65b283196c7fe4f60553eb"
+ "f39c6402c42234e32a356b3e764312a6"
+ "1a5532055716ead6962568f87d3f3f77"
+ "04c6a8d1bcd1bf4d50d6154b6da731b1"
+ "87b58dfd728afa36757a797ac188d1"
+ )
+ ]
+
+ for tv in tvs:
+ key = unhexlify(tv[0])
+ nonce = unhexlify(tv[1])
+ offset = tv[2] * 64
+ pt = unhexlify(tv[3])
+ ct_expect = unhexlify(tv[4])
+
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ if offset != 0:
+ cipher.seek(offset)
+ ct = cipher.encrypt(pt)
+ assert(ct == ct_expect)
+
+
+class XChaCha20Test(unittest.TestCase):
+
+ # From https://tools.ietf.org/html/draft-arciszewski-xchacha-03
+
+ def test_hchacha20(self):
+ # Section 2.2.1
+
+ from Crypto.Cipher.ChaCha20 import _HChaCha20
+
+ key = b"00:01:02:03:04:05:06:07:08:09:0a:0b:0c:0d:0e:0f:10:11:12:13:14:15:16:17:18:19:1a:1b:1c:1d:1e:1f"
+ key = unhexlify(key.replace(b":", b""))
+
+ nonce = b"00:00:00:09:00:00:00:4a:00:00:00:00:31:41:59:27"
+ nonce = unhexlify(nonce.replace(b":", b""))
+
+ subkey = _HChaCha20(key, nonce)
+
+ expected = b"82413b42 27b27bfe d30e4250 8a877d73 a0f9e4d5 8a74a853 c12ec413 26d3ecdc"
+ expected = unhexlify(expected.replace(b" ", b""))
+
+ self.assertEqual(subkey, expected)
+
+ def test_nonce(self):
+ key = b'A' * 32
+ nonce = b'P' * 24
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ self.assertEqual(nonce, cipher.nonce)
+
+ def test_encrypt(self):
+ # Section A.3.2
+
+ pt = b"""
+ 5468652064686f6c65202870726f6e6f756e6365642022646f6c652229206973
+ 20616c736f206b6e6f776e2061732074686520417369617469632077696c6420
+ 646f672c2072656420646f672c20616e642077686973746c696e6720646f672e
+ 2049742069732061626f7574207468652073697a65206f662061204765726d61
+ 6e20736865706865726420627574206c6f6f6b73206d6f7265206c696b652061
+ 206c6f6e672d6c656767656420666f782e205468697320686967686c7920656c
+ 757369766520616e6420736b696c6c6564206a756d70657220697320636c6173
+ 736966696564207769746820776f6c7665732c20636f796f7465732c206a6163
+ 6b616c732c20616e6420666f78657320696e20746865207461786f6e6f6d6963
+ 2066616d696c792043616e696461652e"""
+ pt = unhexlify(pt.replace(b"\n", b"").replace(b" ", b""))
+
+ key = unhexlify(b"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
+ iv = unhexlify(b"404142434445464748494a4b4c4d4e4f5051525354555658")
+
+ ct = b"""
+ 7d0a2e6b7f7c65a236542630294e063b7ab9b555a5d5149aa21e4ae1e4fbce87
+ ecc8e08a8b5e350abe622b2ffa617b202cfad72032a3037e76ffdcdc4376ee05
+ 3a190d7e46ca1de04144850381b9cb29f051915386b8a710b8ac4d027b8b050f
+ 7cba5854e028d564e453b8a968824173fc16488b8970cac828f11ae53cabd201
+ 12f87107df24ee6183d2274fe4c8b1485534ef2c5fbc1ec24bfc3663efaa08bc
+ 047d29d25043532db8391a8a3d776bf4372a6955827ccb0cdd4af403a7ce4c63
+ d595c75a43e045f0cce1f29c8b93bd65afc5974922f214a40b7c402cdb91ae73
+ c0b63615cdad0480680f16515a7ace9d39236464328a37743ffc28f4ddb324f4
+ d0f5bbdc270c65b1749a6efff1fbaa09536175ccd29fb9e6057b307320d31683
+ 8a9c71f70b5b5907a66f7ea49aadc409"""
+ ct = unhexlify(ct.replace(b"\n", b"").replace(b" ", b""))
+
+ cipher = ChaCha20.new(key=key, nonce=iv)
+ cipher.seek(64) # Counter = 1
+ ct_test = cipher.encrypt(pt)
+ self.assertEqual(ct, ct_test)
+
+
+class ByteArrayTest(unittest.TestCase):
+ """Verify we can encrypt or decrypt bytearrays"""
+
+ def runTest(self):
+
+ data = b"0123"
+ key = b"9" * 32
+ nonce = b"t" * 8
+
+ # Encryption
+ data_ba = bytearray(data)
+ key_ba = bytearray(key)
+ nonce_ba = bytearray(nonce)
+
+ cipher1 = ChaCha20.new(key=key, nonce=nonce)
+ ct = cipher1.encrypt(data)
+
+ cipher2 = ChaCha20.new(key=key_ba, nonce=nonce_ba)
+ key_ba[:1] = b'\xFF'
+ nonce_ba[:1] = b'\xFF'
+ ct_test = cipher2.encrypt(data_ba)
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decryption
+ key_ba = bytearray(key)
+ nonce_ba = bytearray(nonce)
+ ct_ba = bytearray(ct)
+
+ cipher3 = ChaCha20.new(key=key_ba, nonce=nonce_ba)
+ key_ba[:1] = b'\xFF'
+ nonce_ba[:1] = b'\xFF'
+ pt_test = cipher3.decrypt(ct_ba)
+
+ self.assertEqual(data, pt_test)
+
+
+class MemoryviewTest(unittest.TestCase):
+ """Verify we can encrypt or decrypt bytearrays"""
+
+ def runTest(self):
+
+ data = b"0123"
+ key = b"9" * 32
+ nonce = b"t" * 8
+
+ # Encryption
+ data_mv = memoryview(bytearray(data))
+ key_mv = memoryview(bytearray(key))
+ nonce_mv = memoryview(bytearray(nonce))
+
+ cipher1 = ChaCha20.new(key=key, nonce=nonce)
+ ct = cipher1.encrypt(data)
+
+ cipher2 = ChaCha20.new(key=key_mv, nonce=nonce_mv)
+ key_mv[:1] = b'\xFF'
+ nonce_mv[:1] = b'\xFF'
+ ct_test = cipher2.encrypt(data_mv)
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decryption
+ key_mv = memoryview(bytearray(key))
+ nonce_mv = memoryview(bytearray(nonce))
+ ct_mv = memoryview(bytearray(ct))
+
+ cipher3 = ChaCha20.new(key=key_mv, nonce=nonce_mv)
+ key_mv[:1] = b'\xFF'
+ nonce_mv[:1] = b'\xFF'
+ pt_test = cipher3.decrypt(ct_mv)
+
+ self.assertEqual(data, pt_test)
+
+
+class ChaCha20_AGL_NIR(unittest.TestCase):
+
+ # From http://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04
+ # and http://tools.ietf.org/html/draft-nir-cfrg-chacha20-poly1305-04
+ tv = [
+ ( "00" * 32,
+ "00" * 8,
+ "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc"
+ "8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11c"
+ "c387b669b2ee6586"
+ "9f07e7be5551387a98ba977c732d080d"
+ "cb0f29a048e3656912c6533e32ee7aed"
+ "29b721769ce64e43d57133b074d839d5"
+ "31ed1f28510afb45ace10a1f4b794d6f"
+ ),
+ ( "00" * 31 + "01",
+ "00" * 8,
+ "4540f05a9f1fb296d7736e7b208e3c96eb4fe1834688d2604f450952"
+ "ed432d41bbe2a0b6ea7566d2a5d1e7e20d42af2c53d792b1c43fea81"
+ "7e9ad275ae546963"
+ "3aeb5224ecf849929b9d828db1ced4dd"
+ "832025e8018b8160b82284f3c949aa5a"
+ "8eca00bbb4a73bdad192b5c42f73f2fd"
+ "4e273644c8b36125a64addeb006c13a0"
+ ),
+ ( "00" * 32,
+ "00" * 7 + "01",
+ "de9cba7bf3d69ef5e786dc63973f653a0b49e015adbff7134fcb7df1"
+ "37821031e85a050278a7084527214f73efc7fa5b5277062eb7a0433e"
+ "445f41e3"
+ ),
+ ( "00" * 32,
+ "01" + "00" * 7,
+ "ef3fdfd6c61578fbf5cf35bd3dd33b8009631634d21e42ac33960bd1"
+ "38e50d32111e4caf237ee53ca8ad6426194a88545ddc497a0b466e7d"
+ "6bbdb0041b2f586b"
+ ),
+ ( "000102030405060708090a0b0c0d0e0f101112131415161718191a1b"
+ "1c1d1e1f",
+ "0001020304050607",
+ "f798a189f195e66982105ffb640bb7757f579da31602fc93ec01ac56"
+ "f85ac3c134a4547b733b46413042c9440049176905d3be59ea1c53f1"
+ "5916155c2be8241a38008b9a26bc35941e2444177c8ade6689de9526"
+ "4986d95889fb60e84629c9bd9a5acb1cc118be563eb9b3a4a472f82e"
+ "09a7e778492b562ef7130e88dfe031c79db9d4f7c7a899151b9a4750"
+ "32b63fc385245fe054e3dd5a97a5f576fe064025d3ce042c566ab2c5"
+ "07b138db853e3d6959660996546cc9c4a6eafdc777c040d70eaf46f7"
+ "6dad3979e5c5360c3317166a1c894c94a371876a94df7628fe4eaaf2"
+ "ccb27d5aaae0ad7ad0f9d4b6ad3b54098746d4524d38407a6deb3ab7"
+ "8fab78c9"
+ ),
+ ( "00" * 32,
+ "00" * 7 + "02",
+ "c2c64d378cd536374ae204b9ef933fcd"
+ "1a8b2288b3dfa49672ab765b54ee27c7"
+ "8a970e0e955c14f3a88e741b97c286f7"
+ "5f8fc299e8148362fa198a39531bed6d"
+ ),
+ ]
+
+ def runTest(self):
+ for (key, nonce, stream) in self.tv:
+ c = ChaCha20.new(key=unhexlify(b(key)), nonce=unhexlify(b(nonce)))
+ ct = unhexlify(b(stream))
+ pt = b("\x00") * len(ct)
+ self.assertEqual(c.encrypt(pt), ct)
+
+
+class TestOutput(unittest.TestCase):
+
+ def runTest(self):
+ # Encrypt/Decrypt data and test output parameter
+
+ key = b'4' * 32
+ nonce = b'5' * 8
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+
+ pt = b'5' * 300
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(len(pt))
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ output = memoryview(bytearray(len(pt)))
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0'*len(pt))
+
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0'*len(pt))
+
+ shorter_output = bytearray(len(pt) - 1)
+
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+
+ cipher = ChaCha20.new(key=key, nonce=nonce)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(ChaCha20Test)
+ tests += list_test_cases(XChaCha20Test)
+ tests.append(ChaCha20_AGL_NIR())
+ tests.append(ByteArrayTest())
+ tests.append(MemoryviewTest())
+ tests.append(TestOutput())
+
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ChaCha20_Poly1305.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ChaCha20_Poly1305.py
new file mode 100644
index 00000000..9129c68d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_ChaCha20_Poly1305.py
@@ -0,0 +1,776 @@
+# ===================================================================
+#
+# Copyright (c) 2018, Helder Eijs
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors_wycheproof
+from Crypto.Util.py3compat import tobytes
+from Crypto.Cipher import ChaCha20_Poly1305
+from Crypto.Hash import SHAKE128
+
+from Crypto.Util.strxor import strxor
+
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+
+class ChaCha20Poly1305Tests(unittest.TestCase):
+
+ key_256 = get_tag_random("key_256", 32)
+ nonce_96 = get_tag_random("nonce_96", 12)
+ data_128 = get_tag_random("data_128", 16)
+
+ def test_loopback(self):
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ pt = get_tag_random("plaintext", 16 * 100)
+ ct = cipher.encrypt(pt)
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_nonce(self):
+ # Nonce can only be 8 or 12 bytes
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=b'H' * 8)
+ self.assertEqual(len(cipher.nonce), 8)
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=b'H' * 12)
+ self.assertEqual(len(cipher.nonce), 12)
+
+ # If not passed, the nonce is created randomly
+ cipher = ChaCha20_Poly1305.new(key=self.key_256)
+ nonce1 = cipher.nonce
+ cipher = ChaCha20_Poly1305.new(key=self.key_256)
+ nonce2 = cipher.nonce
+ self.assertEqual(len(nonce1), 12)
+ self.assertNotEqual(nonce1, nonce2)
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data_128)
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ self.assertEqual(ct, cipher.encrypt(self.data_128))
+
+ def test_nonce_must_be_bytes(self):
+ self.assertRaises(TypeError,
+ ChaCha20_Poly1305.new,
+ key=self.key_256,
+ nonce=u'test12345678')
+
+ def test_nonce_length(self):
+ # nonce can only be 8 or 12 bytes long
+ self.assertRaises(ValueError,
+ ChaCha20_Poly1305.new,
+ key=self.key_256,
+ nonce=b'0' * 7)
+ self.assertRaises(ValueError,
+ ChaCha20_Poly1305.new,
+ key=self.key_256,
+ nonce=b'')
+
+ def test_block_size(self):
+ # Not based on block ciphers
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ self.assertFalse(hasattr(cipher, 'block_size'))
+
+ def test_nonce_attribute(self):
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ self.assertEqual(cipher.nonce, self.nonce_96)
+
+ # By default, a 12 bytes long nonce is randomly generated
+ nonce1 = ChaCha20_Poly1305.new(key=self.key_256).nonce
+ nonce2 = ChaCha20_Poly1305.new(key=self.key_256).nonce
+ self.assertEqual(len(nonce1), 12)
+ self.assertNotEqual(nonce1, nonce2)
+
+ def test_unknown_parameters(self):
+ self.assertRaises(TypeError,
+ ChaCha20_Poly1305.new,
+ key=self.key_256,
+ param=9)
+
+ def test_null_encryption_decryption(self):
+ for func in "encrypt", "decrypt":
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ result = getattr(cipher, func)(b"")
+ self.assertEqual(result, b"")
+
+ def test_either_encrypt_or_decrypt(self):
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.encrypt(b"")
+ self.assertRaises(TypeError, cipher.decrypt, b"")
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.decrypt(b"")
+ self.assertRaises(TypeError, cipher.encrypt, b"")
+
+ def test_data_must_be_bytes(self):
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt, u'test1234567890-*')
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt, u'test1234567890-*')
+
+ def test_mac_len(self):
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ _, mac = cipher.encrypt_and_digest(self.data_128)
+ self.assertEqual(len(mac), 16)
+
+ def test_invalid_mac(self):
+ from Crypto.Util.strxor import strxor_c
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ ct, mac = cipher.encrypt_and_digest(self.data_128)
+
+ invalid_mac = strxor_c(mac, 0x01)
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct,
+ invalid_mac)
+
+ def test_hex_mac(self):
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ mac_hex = cipher.hexdigest()
+ self.assertEqual(cipher.digest(), unhexlify(mac_hex))
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.hexverify(mac_hex)
+
+ def test_message_chunks(self):
+ # Validate that both associated data and plaintext/ciphertext
+ # can be broken up in chunks of arbitrary length
+
+ auth_data = get_tag_random("authenticated data", 127)
+ plaintext = get_tag_random("plaintext", 127)
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.update(auth_data)
+ ciphertext, ref_mac = cipher.encrypt_and_digest(plaintext)
+
+ def break_up(data, chunk_length):
+ return [data[i:i+chunk_length] for i in range(0, len(data),
+ chunk_length)]
+
+ # Encryption
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+
+ for chunk in break_up(auth_data, chunk_length):
+ cipher.update(chunk)
+ pt2 = b""
+ for chunk in break_up(ciphertext, chunk_length):
+ pt2 += cipher.decrypt(chunk)
+ self.assertEqual(plaintext, pt2)
+ cipher.verify(ref_mac)
+
+ # Decryption
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+
+ for chunk in break_up(auth_data, chunk_length):
+ cipher.update(chunk)
+ ct2 = b""
+ for chunk in break_up(plaintext, chunk_length):
+ ct2 += cipher.encrypt(chunk)
+ self.assertEqual(ciphertext, ct2)
+ self.assertEqual(cipher.digest(), ref_mac)
+
+ def test_bytearray(self):
+
+ # Encrypt
+ key_ba = bytearray(self.key_256)
+ nonce_ba = bytearray(self.nonce_96)
+ header_ba = bytearray(self.data_128)
+ data_ba = bytearray(self.data_128)
+
+ cipher1 = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher1.update(self.data_128)
+ ct = cipher1.encrypt(self.data_128)
+ tag = cipher1.digest()
+
+ cipher2 = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ key_ba[:3] = b'\xFF\xFF\xFF'
+ nonce_ba[:3] = b'\xFF\xFF\xFF'
+ cipher2.update(header_ba)
+ header_ba[:3] = b'\xFF\xFF\xFF'
+ ct_test = cipher2.encrypt(data_ba)
+ data_ba[:3] = b'\x99\x99\x99'
+ tag_test = cipher2.digest()
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key_ba = bytearray(self.key_256)
+ nonce_ba = bytearray(self.nonce_96)
+ header_ba = bytearray(self.data_128)
+ ct_ba = bytearray(ct)
+ tag_ba = bytearray(tag)
+ del data_ba
+
+ cipher3 = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ key_ba[:3] = b'\xFF\xFF\xFF'
+ nonce_ba[:3] = b'\xFF\xFF\xFF'
+ cipher3.update(header_ba)
+ header_ba[:3] = b'\xFF\xFF\xFF'
+ pt_test = cipher3.decrypt(ct_ba)
+ ct_ba[:3] = b'\xFF\xFF\xFF'
+ cipher3.verify(tag_ba)
+
+ self.assertEqual(pt_test, self.data_128)
+
+ def test_memoryview(self):
+
+ # Encrypt
+ key_mv = memoryview(bytearray(self.key_256))
+ nonce_mv = memoryview(bytearray(self.nonce_96))
+ header_mv = memoryview(bytearray(self.data_128))
+ data_mv = memoryview(bytearray(self.data_128))
+
+ cipher1 = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher1.update(self.data_128)
+ ct = cipher1.encrypt(self.data_128)
+ tag = cipher1.digest()
+
+ cipher2 = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ key_mv[:3] = b'\xFF\xFF\xFF'
+ nonce_mv[:3] = b'\xFF\xFF\xFF'
+ cipher2.update(header_mv)
+ header_mv[:3] = b'\xFF\xFF\xFF'
+ ct_test = cipher2.encrypt(data_mv)
+ data_mv[:3] = b'\x99\x99\x99'
+ tag_test = cipher2.digest()
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key_mv = memoryview(bytearray(self.key_256))
+ nonce_mv = memoryview(bytearray(self.nonce_96))
+ header_mv = memoryview(bytearray(self.data_128))
+ ct_mv = memoryview(bytearray(ct))
+ tag_mv = memoryview(bytearray(tag))
+ del data_mv
+
+ cipher3 = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ key_mv[:3] = b'\xFF\xFF\xFF'
+ nonce_mv[:3] = b'\xFF\xFF\xFF'
+ cipher3.update(header_mv)
+ header_mv[:3] = b'\xFF\xFF\xFF'
+ pt_test = cipher3.decrypt(ct_mv)
+ ct_mv[:3] = b'\x99\x99\x99'
+ cipher3.verify(tag_mv)
+
+ self.assertEqual(pt_test, self.data_128)
+
+
+class XChaCha20Poly1305Tests(unittest.TestCase):
+
+ def test_nonce(self):
+ # Nonce can only be 24 bytes
+ cipher = ChaCha20_Poly1305.new(key=b'Y' * 32,
+ nonce=b'H' * 24)
+ self.assertEqual(len(cipher.nonce), 24)
+ self.assertEqual(cipher.nonce, b'H' * 24)
+
+ def test_encrypt(self):
+ # From https://tools.ietf.org/html/draft-arciszewski-xchacha-03
+ # Section A.3.1
+
+ pt = b"""
+ 4c616469657320616e642047656e746c656d656e206f662074686520636c6173
+ 73206f66202739393a204966204920636f756c64206f6666657220796f75206f
+ 6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73
+ 637265656e20776f756c642062652069742e"""
+ pt = unhexlify(pt.replace(b"\n", b"").replace(b" ", b""))
+
+ aad = unhexlify(b"50515253c0c1c2c3c4c5c6c7")
+ key = unhexlify(b"808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
+ iv = unhexlify(b"404142434445464748494a4b4c4d4e4f5051525354555657")
+
+ ct = b"""
+ bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb
+ 731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b452
+ 2f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff9
+ 21f9664c97637da9768812f615c68b13b52e"""
+ ct = unhexlify(ct.replace(b"\n", b"").replace(b" ", b""))
+
+ tag = unhexlify(b"c0875924c1c7987947deafd8780acf49")
+
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=iv)
+ cipher.update(aad)
+ ct_test, tag_test = cipher.encrypt_and_digest(pt)
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=iv)
+ cipher.update(aad)
+ cipher.decrypt_and_verify(ct, tag)
+
+
+class ChaCha20Poly1305FSMTests(unittest.TestCase):
+
+ key_256 = get_tag_random("key_256", 32)
+ nonce_96 = get_tag_random("nonce_96", 12)
+ data_128 = get_tag_random("data_128", 16)
+
+ def test_valid_init_encrypt_decrypt_digest_verify(self):
+ # No authenticated data, fixed plaintext
+ # Verify path INIT->ENCRYPT->DIGEST
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data_128)
+ mac = cipher.digest()
+
+ # Verify path INIT->DECRYPT->VERIFY
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+
+ def test_valid_init_update_digest_verify(self):
+ # No plaintext, fixed authenticated data
+ # Verify path INIT->UPDATE->DIGEST
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->VERIFY
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ cipher.verify(mac)
+
+ def test_valid_full_path(self):
+ # Fixed authenticated data, fixed plaintext
+ # Verify path INIT->UPDATE->ENCRYPT->DIGEST
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ ct = cipher.encrypt(self.data_128)
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->DECRYPT->VERIFY
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+
+ def test_valid_init_digest(self):
+ # Verify path INIT->DIGEST
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.digest()
+
+ def test_valid_init_verify(self):
+ # Verify path INIT->VERIFY
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ mac = cipher.digest()
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.verify(mac)
+
+ def test_valid_multiple_encrypt_or_decrypt(self):
+ for method_name in "encrypt", "decrypt":
+ for auth_data in (None, b"333", self.data_128,
+ self.data_128 + b"3"):
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ if auth_data is not None:
+ cipher.update(auth_data)
+ method = getattr(cipher, method_name)
+ method(self.data_128)
+ method(self.data_128)
+ method(self.data_128)
+ method(self.data_128)
+
+ def test_valid_multiple_digest_or_verify(self):
+ # Multiple calls to digest
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ first_mac = cipher.digest()
+ for x in range(4):
+ self.assertEqual(first_mac, cipher.digest())
+
+ # Multiple calls to verify
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ for x in range(5):
+ cipher.verify(first_mac)
+
+ def test_valid_encrypt_and_digest_decrypt_and_verify(self):
+ # encrypt_and_digest
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ ct, mac = cipher.encrypt_and_digest(self.data_128)
+
+ # decrypt_and_verify
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ pt = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(self.data_128, pt)
+
+ def test_invalid_mixing_encrypt_decrypt(self):
+ # Once per method, with or without assoc. data
+ for method1_name, method2_name in (("encrypt", "decrypt"),
+ ("decrypt", "encrypt")):
+ for assoc_data_present in (True, False):
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ if assoc_data_present:
+ cipher.update(self.data_128)
+ getattr(cipher, method1_name)(self.data_128)
+ self.assertRaises(TypeError, getattr(cipher, method2_name),
+ self.data_128)
+
+ def test_invalid_encrypt_or_update_after_digest(self):
+ for method_name in "encrypt", "update":
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.encrypt(self.data_128)
+ cipher.digest()
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data_128)
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.encrypt_and_digest(self.data_128)
+
+ def test_invalid_decrypt_or_update_after_verify(self):
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data_128)
+ mac = cipher.digest()
+
+ for method_name in "decrypt", "update":
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data_128)
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data_128)
+
+ cipher = ChaCha20_Poly1305.new(key=self.key_256,
+ nonce=self.nonce_96)
+ cipher.decrypt_and_verify(ct, mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data_128)
+
+
+def compact(x):
+ return unhexlify(x.replace(" ", "").replace(":", ""))
+
+
+class TestVectorsRFC(unittest.TestCase):
+ """Test cases from RFC7539"""
+
+ # AAD, PT, CT, MAC, KEY, NONCE
+ test_vectors_hex = [
+ ( '50 51 52 53 c0 c1 c2 c3 c4 c5 c6 c7',
+ '4c 61 64 69 65 73 20 61 6e 64 20 47 65 6e 74 6c'
+ '65 6d 65 6e 20 6f 66 20 74 68 65 20 63 6c 61 73'
+ '73 20 6f 66 20 27 39 39 3a 20 49 66 20 49 20 63'
+ '6f 75 6c 64 20 6f 66 66 65 72 20 79 6f 75 20 6f'
+ '6e 6c 79 20 6f 6e 65 20 74 69 70 20 66 6f 72 20'
+ '74 68 65 20 66 75 74 75 72 65 2c 20 73 75 6e 73'
+ '63 72 65 65 6e 20 77 6f 75 6c 64 20 62 65 20 69'
+ '74 2e',
+ 'd3 1a 8d 34 64 8e 60 db 7b 86 af bc 53 ef 7e c2'
+ 'a4 ad ed 51 29 6e 08 fe a9 e2 b5 a7 36 ee 62 d6'
+ '3d be a4 5e 8c a9 67 12 82 fa fb 69 da 92 72 8b'
+ '1a 71 de 0a 9e 06 0b 29 05 d6 a5 b6 7e cd 3b 36'
+ '92 dd bd 7f 2d 77 8b 8c 98 03 ae e3 28 09 1b 58'
+ 'fa b3 24 e4 fa d6 75 94 55 85 80 8b 48 31 d7 bc'
+ '3f f4 de f0 8e 4b 7a 9d e5 76 d2 65 86 ce c6 4b'
+ '61 16',
+ '1a:e1:0b:59:4f:09:e2:6a:7e:90:2e:cb:d0:60:06:91',
+ '80 81 82 83 84 85 86 87 88 89 8a 8b 8c 8d 8e 8f'
+ '90 91 92 93 94 95 96 97 98 99 9a 9b 9c 9d 9e 9f',
+ '07 00 00 00' + '40 41 42 43 44 45 46 47',
+ ),
+ ( 'f3 33 88 86 00 00 00 00 00 00 4e 91',
+ '49 6e 74 65 72 6e 65 74 2d 44 72 61 66 74 73 20'
+ '61 72 65 20 64 72 61 66 74 20 64 6f 63 75 6d 65'
+ '6e 74 73 20 76 61 6c 69 64 20 66 6f 72 20 61 20'
+ '6d 61 78 69 6d 75 6d 20 6f 66 20 73 69 78 20 6d'
+ '6f 6e 74 68 73 20 61 6e 64 20 6d 61 79 20 62 65'
+ '20 75 70 64 61 74 65 64 2c 20 72 65 70 6c 61 63'
+ '65 64 2c 20 6f 72 20 6f 62 73 6f 6c 65 74 65 64'
+ '20 62 79 20 6f 74 68 65 72 20 64 6f 63 75 6d 65'
+ '6e 74 73 20 61 74 20 61 6e 79 20 74 69 6d 65 2e'
+ '20 49 74 20 69 73 20 69 6e 61 70 70 72 6f 70 72'
+ '69 61 74 65 20 74 6f 20 75 73 65 20 49 6e 74 65'
+ '72 6e 65 74 2d 44 72 61 66 74 73 20 61 73 20 72'
+ '65 66 65 72 65 6e 63 65 20 6d 61 74 65 72 69 61'
+ '6c 20 6f 72 20 74 6f 20 63 69 74 65 20 74 68 65'
+ '6d 20 6f 74 68 65 72 20 74 68 61 6e 20 61 73 20'
+ '2f e2 80 9c 77 6f 72 6b 20 69 6e 20 70 72 6f 67'
+ '72 65 73 73 2e 2f e2 80 9d',
+ '64 a0 86 15 75 86 1a f4 60 f0 62 c7 9b e6 43 bd'
+ '5e 80 5c fd 34 5c f3 89 f1 08 67 0a c7 6c 8c b2'
+ '4c 6c fc 18 75 5d 43 ee a0 9e e9 4e 38 2d 26 b0'
+ 'bd b7 b7 3c 32 1b 01 00 d4 f0 3b 7f 35 58 94 cf'
+ '33 2f 83 0e 71 0b 97 ce 98 c8 a8 4a bd 0b 94 81'
+ '14 ad 17 6e 00 8d 33 bd 60 f9 82 b1 ff 37 c8 55'
+ '97 97 a0 6e f4 f0 ef 61 c1 86 32 4e 2b 35 06 38'
+ '36 06 90 7b 6a 7c 02 b0 f9 f6 15 7b 53 c8 67 e4'
+ 'b9 16 6c 76 7b 80 4d 46 a5 9b 52 16 cd e7 a4 e9'
+ '90 40 c5 a4 04 33 22 5e e2 82 a1 b0 a0 6c 52 3e'
+ 'af 45 34 d7 f8 3f a1 15 5b 00 47 71 8c bc 54 6a'
+ '0d 07 2b 04 b3 56 4e ea 1b 42 22 73 f5 48 27 1a'
+ '0b b2 31 60 53 fa 76 99 19 55 eb d6 31 59 43 4e'
+ 'ce bb 4e 46 6d ae 5a 10 73 a6 72 76 27 09 7a 10'
+ '49 e6 17 d9 1d 36 10 94 fa 68 f0 ff 77 98 71 30'
+ '30 5b ea ba 2e da 04 df 99 7b 71 4d 6c 6f 2c 29'
+ 'a6 ad 5c b4 02 2b 02 70 9b',
+ 'ee ad 9d 67 89 0c bb 22 39 23 36 fe a1 85 1f 38',
+ '1c 92 40 a5 eb 55 d3 8a f3 33 88 86 04 f6 b5 f0'
+ '47 39 17 c1 40 2b 80 09 9d ca 5c bc 20 70 75 c0',
+ '00 00 00 00 01 02 03 04 05 06 07 08',
+ )
+ ]
+
+ test_vectors = [[unhexlify(x.replace(" ", "").replace(":", "")) for x in tv] for tv in test_vectors_hex]
+
+ def runTest(self):
+ for assoc_data, pt, ct, mac, key, nonce in self.test_vectors:
+ # Encrypt
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ cipher.update(assoc_data)
+ ct2, mac2 = cipher.encrypt_and_digest(pt)
+ self.assertEqual(ct, ct2)
+ self.assertEqual(mac, mac2)
+
+ # Decrypt
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ cipher.update(assoc_data)
+ pt2 = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(pt, pt2)
+
+
+class TestVectorsWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._id = "None"
+
+ def load_tests(self, filename):
+
+ def filter_tag(group):
+ return group['tagSize'] // 8
+
+ def filter_algo(root):
+ return root['algorithm']
+
+ result = load_test_vectors_wycheproof(("Cipher", "wycheproof"),
+ filename,
+ "Wycheproof ChaCha20-Poly1305",
+ root_tag={'algo': filter_algo},
+ group_tag={'tag_size': filter_tag})
+ return result
+
+ def setUp(self):
+ self.tv = []
+ self.tv.extend(self.load_tests("chacha20_poly1305_test.json"))
+ self.tv.extend(self.load_tests("xchacha20_poly1305_test.json"))
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_encrypt(self, tv):
+ self._id = "Wycheproof Encrypt %s Test #%s" % (tv.algo, tv.id)
+
+ try:
+ cipher = ChaCha20_Poly1305.new(key=tv.key, nonce=tv.iv)
+ except ValueError as e:
+ assert len(tv.iv) not in (8, 12) and "Nonce must be" in str(e)
+ return
+
+ cipher.update(tv.aad)
+ ct, tag = cipher.encrypt_and_digest(tv.msg)
+ if tv.valid:
+ self.assertEqual(ct, tv.ct)
+ self.assertEqual(tag, tv.tag)
+ self.warn(tv)
+
+ def test_decrypt(self, tv):
+ self._id = "Wycheproof Decrypt %s Test #%s" % (tv.algo, tv.id)
+
+ try:
+ cipher = ChaCha20_Poly1305.new(key=tv.key, nonce=tv.iv)
+ except ValueError as e:
+ assert len(tv.iv) not in (8, 12) and "Nonce must be" in str(e)
+ return
+
+ cipher.update(tv.aad)
+ try:
+ pt = cipher.decrypt_and_verify(tv.ct, tv.tag)
+ except ValueError:
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.assertEqual(pt, tv.msg)
+ self.warn(tv)
+
+ def test_corrupt_decrypt(self, tv):
+ self._id = "Wycheproof Corrupt Decrypt ChaCha20-Poly1305 Test #" + str(tv.id)
+ if len(tv.iv) == 0 or len(tv.ct) < 1:
+ return
+ cipher = ChaCha20_Poly1305.new(key=tv.key, nonce=tv.iv)
+ cipher.update(tv.aad)
+ ct_corrupt = strxor(tv.ct, b"\x00" * (len(tv.ct) - 1) + b"\x01")
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct_corrupt, tv.tag)
+
+ def runTest(self):
+
+ for tv in self.tv:
+ self.test_encrypt(tv)
+ self.test_decrypt(tv)
+ self.test_corrupt_decrypt(tv)
+
+
+class TestOutput(unittest.TestCase):
+
+ def runTest(self):
+ # Encrypt/Decrypt data and test output parameter
+
+ key = b'4' * 32
+ nonce = b'5' * 12
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+
+ pt = b'5' * 16
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(16)
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ output = memoryview(bytearray(16))
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0'*16)
+
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0'*16)
+
+ shorter_output = bytearray(7)
+
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+
+ cipher = ChaCha20_Poly1305.new(key=key, nonce=nonce)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+def get_tests(config={}):
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ tests = []
+ tests += list_test_cases(ChaCha20Poly1305Tests)
+ tests += list_test_cases(XChaCha20Poly1305Tests)
+ tests += list_test_cases(ChaCha20Poly1305FSMTests)
+ tests += [TestVectorsRFC()]
+ tests += [TestVectorsWycheproof(wycheproof_warnings)]
+ tests += [TestOutput()]
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_DES.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_DES.py
new file mode 100644
index 00000000..ee261bce
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_DES.py
@@ -0,0 +1,374 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/DES.py: Self-test for the (Single) DES cipher
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Cipher.DES"""
+
+import unittest
+
+from Crypto.Cipher import DES
+
+# This is a list of (plaintext, ciphertext, key, description) tuples.
+SP800_17_B1_KEY = '01' * 8
+SP800_17_B2_PT = '00' * 8
+test_data = [
+ # Test vectors from Appendix A of NIST SP 800-17
+ # "Modes of Operation Validation System (MOVS): Requirements and Procedures"
+ # http://csrc.nist.gov/publications/nistpubs/800-17/800-17.pdf
+
+ # Appendix A - "Sample Round Outputs for the DES"
+ ('0000000000000000', '82dcbafbdeab6602', '10316e028c8f3b4a',
+ "NIST SP800-17 A"),
+
+ # Table B.1 - Variable Plaintext Known Answer Test
+ ('8000000000000000', '95f8a5e5dd31d900', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #0'),
+ ('4000000000000000', 'dd7f121ca5015619', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #1'),
+ ('2000000000000000', '2e8653104f3834ea', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #2'),
+ ('1000000000000000', '4bd388ff6cd81d4f', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #3'),
+ ('0800000000000000', '20b9e767b2fb1456', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #4'),
+ ('0400000000000000', '55579380d77138ef', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #5'),
+ ('0200000000000000', '6cc5defaaf04512f', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #6'),
+ ('0100000000000000', '0d9f279ba5d87260', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #7'),
+ ('0080000000000000', 'd9031b0271bd5a0a', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #8'),
+ ('0040000000000000', '424250b37c3dd951', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #9'),
+ ('0020000000000000', 'b8061b7ecd9a21e5', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #10'),
+ ('0010000000000000', 'f15d0f286b65bd28', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #11'),
+ ('0008000000000000', 'add0cc8d6e5deba1', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #12'),
+ ('0004000000000000', 'e6d5f82752ad63d1', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #13'),
+ ('0002000000000000', 'ecbfe3bd3f591a5e', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #14'),
+ ('0001000000000000', 'f356834379d165cd', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #15'),
+ ('0000800000000000', '2b9f982f20037fa9', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #16'),
+ ('0000400000000000', '889de068a16f0be6', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #17'),
+ ('0000200000000000', 'e19e275d846a1298', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #18'),
+ ('0000100000000000', '329a8ed523d71aec', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #19'),
+ ('0000080000000000', 'e7fce22557d23c97', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #20'),
+ ('0000040000000000', '12a9f5817ff2d65d', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #21'),
+ ('0000020000000000', 'a484c3ad38dc9c19', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #22'),
+ ('0000010000000000', 'fbe00a8a1ef8ad72', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #23'),
+ ('0000008000000000', '750d079407521363', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #24'),
+ ('0000004000000000', '64feed9c724c2faf', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #25'),
+ ('0000002000000000', 'f02b263b328e2b60', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #26'),
+ ('0000001000000000', '9d64555a9a10b852', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #27'),
+ ('0000000800000000', 'd106ff0bed5255d7', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #28'),
+ ('0000000400000000', 'e1652c6b138c64a5', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #29'),
+ ('0000000200000000', 'e428581186ec8f46', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #30'),
+ ('0000000100000000', 'aeb5f5ede22d1a36', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #31'),
+ ('0000000080000000', 'e943d7568aec0c5c', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #32'),
+ ('0000000040000000', 'df98c8276f54b04b', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #33'),
+ ('0000000020000000', 'b160e4680f6c696f', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #34'),
+ ('0000000010000000', 'fa0752b07d9c4ab8', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #35'),
+ ('0000000008000000', 'ca3a2b036dbc8502', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #36'),
+ ('0000000004000000', '5e0905517bb59bcf', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #37'),
+ ('0000000002000000', '814eeb3b91d90726', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #38'),
+ ('0000000001000000', '4d49db1532919c9f', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #39'),
+ ('0000000000800000', '25eb5fc3f8cf0621', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #40'),
+ ('0000000000400000', 'ab6a20c0620d1c6f', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #41'),
+ ('0000000000200000', '79e90dbc98f92cca', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #42'),
+ ('0000000000100000', '866ecedd8072bb0e', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #43'),
+ ('0000000000080000', '8b54536f2f3e64a8', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #44'),
+ ('0000000000040000', 'ea51d3975595b86b', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #45'),
+ ('0000000000020000', 'caffc6ac4542de31', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #46'),
+ ('0000000000010000', '8dd45a2ddf90796c', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #47'),
+ ('0000000000008000', '1029d55e880ec2d0', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #48'),
+ ('0000000000004000', '5d86cb23639dbea9', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #49'),
+ ('0000000000002000', '1d1ca853ae7c0c5f', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #50'),
+ ('0000000000001000', 'ce332329248f3228', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #51'),
+ ('0000000000000800', '8405d1abe24fb942', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #52'),
+ ('0000000000000400', 'e643d78090ca4207', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #53'),
+ ('0000000000000200', '48221b9937748a23', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #54'),
+ ('0000000000000100', 'dd7c0bbd61fafd54', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #55'),
+ ('0000000000000080', '2fbc291a570db5c4', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #56'),
+ ('0000000000000040', 'e07c30d7e4e26e12', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #57'),
+ ('0000000000000020', '0953e2258e8e90a1', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #58'),
+ ('0000000000000010', '5b711bc4ceebf2ee', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #59'),
+ ('0000000000000008', 'cc083f1e6d9e85f6', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #60'),
+ ('0000000000000004', 'd2fd8867d50d2dfe', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #61'),
+ ('0000000000000002', '06e7ea22ce92708f', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #62'),
+ ('0000000000000001', '166b40b44aba4bd6', SP800_17_B1_KEY,
+ 'NIST SP800-17 B.1 #63'),
+
+ # Table B.2 - Variable Key Known Answer Test
+ (SP800_17_B2_PT, '95a8d72813daa94d', '8001010101010101',
+ 'NIST SP800-17 B.2 #0'),
+ (SP800_17_B2_PT, '0eec1487dd8c26d5', '4001010101010101',
+ 'NIST SP800-17 B.2 #1'),
+ (SP800_17_B2_PT, '7ad16ffb79c45926', '2001010101010101',
+ 'NIST SP800-17 B.2 #2'),
+ (SP800_17_B2_PT, 'd3746294ca6a6cf3', '1001010101010101',
+ 'NIST SP800-17 B.2 #3'),
+ (SP800_17_B2_PT, '809f5f873c1fd761', '0801010101010101',
+ 'NIST SP800-17 B.2 #4'),
+ (SP800_17_B2_PT, 'c02faffec989d1fc', '0401010101010101',
+ 'NIST SP800-17 B.2 #5'),
+ (SP800_17_B2_PT, '4615aa1d33e72f10', '0201010101010101',
+ 'NIST SP800-17 B.2 #6'),
+ (SP800_17_B2_PT, '2055123350c00858', '0180010101010101',
+ 'NIST SP800-17 B.2 #7'),
+ (SP800_17_B2_PT, 'df3b99d6577397c8', '0140010101010101',
+ 'NIST SP800-17 B.2 #8'),
+ (SP800_17_B2_PT, '31fe17369b5288c9', '0120010101010101',
+ 'NIST SP800-17 B.2 #9'),
+ (SP800_17_B2_PT, 'dfdd3cc64dae1642', '0110010101010101',
+ 'NIST SP800-17 B.2 #10'),
+ (SP800_17_B2_PT, '178c83ce2b399d94', '0108010101010101',
+ 'NIST SP800-17 B.2 #11'),
+ (SP800_17_B2_PT, '50f636324a9b7f80', '0104010101010101',
+ 'NIST SP800-17 B.2 #12'),
+ (SP800_17_B2_PT, 'a8468ee3bc18f06d', '0102010101010101',
+ 'NIST SP800-17 B.2 #13'),
+ (SP800_17_B2_PT, 'a2dc9e92fd3cde92', '0101800101010101',
+ 'NIST SP800-17 B.2 #14'),
+ (SP800_17_B2_PT, 'cac09f797d031287', '0101400101010101',
+ 'NIST SP800-17 B.2 #15'),
+ (SP800_17_B2_PT, '90ba680b22aeb525', '0101200101010101',
+ 'NIST SP800-17 B.2 #16'),
+ (SP800_17_B2_PT, 'ce7a24f350e280b6', '0101100101010101',
+ 'NIST SP800-17 B.2 #17'),
+ (SP800_17_B2_PT, '882bff0aa01a0b87', '0101080101010101',
+ 'NIST SP800-17 B.2 #18'),
+ (SP800_17_B2_PT, '25610288924511c2', '0101040101010101',
+ 'NIST SP800-17 B.2 #19'),
+ (SP800_17_B2_PT, 'c71516c29c75d170', '0101020101010101',
+ 'NIST SP800-17 B.2 #20'),
+ (SP800_17_B2_PT, '5199c29a52c9f059', '0101018001010101',
+ 'NIST SP800-17 B.2 #21'),
+ (SP800_17_B2_PT, 'c22f0a294a71f29f', '0101014001010101',
+ 'NIST SP800-17 B.2 #22'),
+ (SP800_17_B2_PT, 'ee371483714c02ea', '0101012001010101',
+ 'NIST SP800-17 B.2 #23'),
+ (SP800_17_B2_PT, 'a81fbd448f9e522f', '0101011001010101',
+ 'NIST SP800-17 B.2 #24'),
+ (SP800_17_B2_PT, '4f644c92e192dfed', '0101010801010101',
+ 'NIST SP800-17 B.2 #25'),
+ (SP800_17_B2_PT, '1afa9a66a6df92ae', '0101010401010101',
+ 'NIST SP800-17 B.2 #26'),
+ (SP800_17_B2_PT, 'b3c1cc715cb879d8', '0101010201010101',
+ 'NIST SP800-17 B.2 #27'),
+ (SP800_17_B2_PT, '19d032e64ab0bd8b', '0101010180010101',
+ 'NIST SP800-17 B.2 #28'),
+ (SP800_17_B2_PT, '3cfaa7a7dc8720dc', '0101010140010101',
+ 'NIST SP800-17 B.2 #29'),
+ (SP800_17_B2_PT, 'b7265f7f447ac6f3', '0101010120010101',
+ 'NIST SP800-17 B.2 #30'),
+ (SP800_17_B2_PT, '9db73b3c0d163f54', '0101010110010101',
+ 'NIST SP800-17 B.2 #31'),
+ (SP800_17_B2_PT, '8181b65babf4a975', '0101010108010101',
+ 'NIST SP800-17 B.2 #32'),
+ (SP800_17_B2_PT, '93c9b64042eaa240', '0101010104010101',
+ 'NIST SP800-17 B.2 #33'),
+ (SP800_17_B2_PT, '5570530829705592', '0101010102010101',
+ 'NIST SP800-17 B.2 #34'),
+ (SP800_17_B2_PT, '8638809e878787a0', '0101010101800101',
+ 'NIST SP800-17 B.2 #35'),
+ (SP800_17_B2_PT, '41b9a79af79ac208', '0101010101400101',
+ 'NIST SP800-17 B.2 #36'),
+ (SP800_17_B2_PT, '7a9be42f2009a892', '0101010101200101',
+ 'NIST SP800-17 B.2 #37'),
+ (SP800_17_B2_PT, '29038d56ba6d2745', '0101010101100101',
+ 'NIST SP800-17 B.2 #38'),
+ (SP800_17_B2_PT, '5495c6abf1e5df51', '0101010101080101',
+ 'NIST SP800-17 B.2 #39'),
+ (SP800_17_B2_PT, 'ae13dbd561488933', '0101010101040101',
+ 'NIST SP800-17 B.2 #40'),
+ (SP800_17_B2_PT, '024d1ffa8904e389', '0101010101020101',
+ 'NIST SP800-17 B.2 #41'),
+ (SP800_17_B2_PT, 'd1399712f99bf02e', '0101010101018001',
+ 'NIST SP800-17 B.2 #42'),
+ (SP800_17_B2_PT, '14c1d7c1cffec79e', '0101010101014001',
+ 'NIST SP800-17 B.2 #43'),
+ (SP800_17_B2_PT, '1de5279dae3bed6f', '0101010101012001',
+ 'NIST SP800-17 B.2 #44'),
+ (SP800_17_B2_PT, 'e941a33f85501303', '0101010101011001',
+ 'NIST SP800-17 B.2 #45'),
+ (SP800_17_B2_PT, 'da99dbbc9a03f379', '0101010101010801',
+ 'NIST SP800-17 B.2 #46'),
+ (SP800_17_B2_PT, 'b7fc92f91d8e92e9', '0101010101010401',
+ 'NIST SP800-17 B.2 #47'),
+ (SP800_17_B2_PT, 'ae8e5caa3ca04e85', '0101010101010201',
+ 'NIST SP800-17 B.2 #48'),
+ (SP800_17_B2_PT, '9cc62df43b6eed74', '0101010101010180',
+ 'NIST SP800-17 B.2 #49'),
+ (SP800_17_B2_PT, 'd863dbb5c59a91a0', '0101010101010140',
+ 'NIST SP800-17 B.2 #50'),
+ (SP800_17_B2_PT, 'a1ab2190545b91d7', '0101010101010120',
+ 'NIST SP800-17 B.2 #51'),
+ (SP800_17_B2_PT, '0875041e64c570f7', '0101010101010110',
+ 'NIST SP800-17 B.2 #52'),
+ (SP800_17_B2_PT, '5a594528bebef1cc', '0101010101010108',
+ 'NIST SP800-17 B.2 #53'),
+ (SP800_17_B2_PT, 'fcdb3291de21f0c0', '0101010101010104',
+ 'NIST SP800-17 B.2 #54'),
+ (SP800_17_B2_PT, '869efd7f9f265a09', '0101010101010102',
+ 'NIST SP800-17 B.2 #55'),
+]
+
+class RonRivestTest(unittest.TestCase):
+ """ Ronald L. Rivest's DES test, see
+ http://people.csail.mit.edu/rivest/Destest.txt
+ ABSTRACT
+ --------
+
+ We present a simple way to test the correctness of a DES implementation:
+ Use the recurrence relation:
+
+ X0 = 9474B8E8C73BCA7D (hexadecimal)
+
+ X(i+1) = IF (i is even) THEN E(Xi,Xi) ELSE D(Xi,Xi)
+
+ to compute a sequence of 64-bit values: X0, X1, X2, ..., X16. Here
+ E(X,K) denotes the DES encryption of X using key K, and D(X,K) denotes
+ the DES decryption of X using key K. If you obtain
+
+ X16 = 1B1A2DDB4C642438
+
+ your implementation does not have any of the 36,568 possible single-fault
+ errors described herein.
+ """
+ def runTest(self):
+ from binascii import b2a_hex
+
+ X = []
+ X[0:] = [b'\x94\x74\xB8\xE8\xC7\x3B\xCA\x7D']
+
+ for i in range(16):
+ c = DES.new(X[i],DES.MODE_ECB)
+ if not (i&1): # (num&1) returns 1 for odd numbers
+ X[i+1:] = [c.encrypt(X[i])] # even
+ else:
+ X[i+1:] = [c.decrypt(X[i])] # odd
+
+ self.assertEqual(b2a_hex(X[16]),
+ b2a_hex(b'\x1B\x1A\x2D\xDB\x4C\x64\x24\x38'))
+
+
+class TestOutput(unittest.TestCase):
+
+ def runTest(self):
+ # Encrypt/Decrypt data and test output parameter
+
+ cipher = DES.new(b'4'*8, DES.MODE_ECB)
+
+ pt = b'5' * 8
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(8)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ output = memoryview(bytearray(8))
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0'*8)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0'*8)
+
+ shorter_output = bytearray(7)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+def get_tests(config={}):
+ from .common import make_block_tests
+ tests = make_block_tests(DES, "DES", test_data)
+ tests += [RonRivestTest()]
+ tests += [TestOutput()]
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_DES3.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_DES3.py
new file mode 100644
index 00000000..8d6a6485
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_DES3.py
@@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/DES3.py: Self-test for the Triple-DES cipher
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Cipher.DES3"""
+
+import unittest
+from binascii import hexlify, unhexlify
+
+from Crypto.Cipher import DES3
+
+from Crypto.Util.strxor import strxor_c
+from Crypto.Util.py3compat import bchr, tostr
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+
+# This is a list of (plaintext, ciphertext, key, description) tuples.
+test_data = [
+ # Test vector from Appendix B of NIST SP 800-67
+ # "Recommendation for the Triple Data Encryption Algorithm (TDEA) Block
+ # Cipher"
+ # http://csrc.nist.gov/publications/nistpubs/800-67/SP800-67.pdf
+ ('54686520717566636b2062726f776e20666f78206a756d70',
+ 'a826fd8ce53b855fcce21c8112256fe668d5c05dd9b6b900',
+ '0123456789abcdef23456789abcdef01456789abcdef0123',
+ 'NIST SP800-67 B.1'),
+
+ # This test is designed to test the DES3 API, not the correctness of the
+ # output.
+ ('21e81b7ade88a259', '5c577d4d9b20c0f8',
+ '9b397ebf81b1181e282f4bb8adbadc6b', 'Two-key 3DES'),
+]
+
+# NIST CAVP test vectors
+
+nist_tdes_mmt_files = ("TECBMMT2.rsp", "TECBMMT3.rsp")
+
+for tdes_file in nist_tdes_mmt_files:
+
+ test_vectors = load_test_vectors(
+ ("Cipher", "TDES"),
+ tdes_file,
+ "TDES ECB (%s)" % tdes_file,
+ {"count": lambda x: int(x)}) or []
+
+ for index, tv in enumerate(test_vectors):
+
+ # The test vector file contains some directive lines
+ if isinstance(tv, str):
+ continue
+
+ key = tv.key1 + tv.key2 + tv.key3
+ test_data_item = (tostr(hexlify(tv.plaintext)),
+ tostr(hexlify(tv.ciphertext)),
+ tostr(hexlify(key)),
+ "%s (%s)" % (tdes_file, index))
+ test_data.append(test_data_item)
+
+
+class CheckParity(unittest.TestCase):
+
+ def test_parity_option2(self):
+ before_2k = unhexlify("CABF326FA56734324FFCCABCDEFACABF")
+ after_2k = DES3.adjust_key_parity(before_2k)
+ self.assertEqual(after_2k,
+ unhexlify("CBBF326EA46734324FFDCBBCDFFBCBBF"))
+
+ def test_parity_option3(self):
+ before_3k = unhexlify("AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBCCCCCCCCCCCCCCCC")
+ after_3k = DES3.adjust_key_parity(before_3k)
+ self.assertEqual(after_3k,
+ unhexlify("ABABABABABABABABBABABABABABABABACDCDCDCDCDCDCDCD"))
+
+ def test_degradation(self):
+ sub_key1 = bchr(1) * 8
+ sub_key2 = bchr(255) * 8
+
+ # K1 == K2
+ self.assertRaises(ValueError, DES3.adjust_key_parity,
+ sub_key1 * 2 + sub_key2)
+
+ # K2 == K3
+ self.assertRaises(ValueError, DES3.adjust_key_parity,
+ sub_key1 + sub_key2 * 2)
+
+ # K1 == K2 == K3
+ self.assertRaises(ValueError, DES3.adjust_key_parity,
+ sub_key1 * 3)
+
+ # K1 == K2 (with different parity)
+ self.assertRaises(ValueError, DES3.adjust_key_parity,
+ sub_key1 + strxor_c(sub_key1, 1) + sub_key2)
+
+
+class DegenerateToDESTest(unittest.TestCase):
+
+ def runTest(self):
+ sub_key1 = bchr(1) * 8
+ sub_key2 = bchr(255) * 8
+
+ # K1 == K2
+ self.assertRaises(ValueError, DES3.new,
+ sub_key1 * 2 + sub_key2,
+ DES3.MODE_ECB)
+
+ # K2 == K3
+ self.assertRaises(ValueError, DES3.new,
+ sub_key1 + sub_key2 * 2,
+ DES3.MODE_ECB)
+
+ # K1 == K2 == K3
+ self.assertRaises(ValueError, DES3.new,
+ sub_key1 * 3,
+ DES3.MODE_ECB)
+
+ # K2 == K3 (parity is ignored)
+ self.assertRaises(ValueError, DES3.new,
+ sub_key1 + sub_key2 + strxor_c(sub_key2, 0x1),
+ DES3.MODE_ECB)
+
+
+class TestOutput(unittest.TestCase):
+
+ def runTest(self):
+ # Encrypt/Decrypt data and test output parameter
+
+ cipher = DES3.new(b'4'*8 + b'G'*8 + b'T'*8, DES3.MODE_ECB)
+
+ pt = b'5' * 16
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(16)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ output = memoryview(bytearray(16))
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0'*16)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0'*16)
+
+ shorter_output = bytearray(7)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+def get_tests(config={}):
+ from .common import make_block_tests
+
+ tests = []
+ tests = make_block_tests(DES3, "DES3", test_data)
+ tests.append(DegenerateToDESTest())
+ tests += list_test_cases(CheckParity)
+ tests += [TestOutput()]
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+
+ def suite():
+ unittest.TestSuite(get_tests())
+
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_EAX.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_EAX.py
new file mode 100644
index 00000000..7dbee2be
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_EAX.py
@@ -0,0 +1,773 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors_wycheproof
+from Crypto.Util.py3compat import tobytes, bchr
+from Crypto.Cipher import AES, DES3
+from Crypto.Hash import SHAKE128
+
+from Crypto.Util.strxor import strxor
+
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+
+class EaxTests(unittest.TestCase):
+
+ key_128 = get_tag_random("key_128", 16)
+ key_192 = get_tag_random("key_192", 16)
+ nonce_96 = get_tag_random("nonce_128", 12)
+ data_128 = get_tag_random("data_128", 16)
+
+ def test_loopback_128(self):
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ pt = get_tag_random("plaintext", 16 * 100)
+ ct = cipher.encrypt(pt)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_loopback_64(self):
+ cipher = DES3.new(self.key_192, DES3.MODE_EAX, nonce=self.nonce_96)
+ pt = get_tag_random("plaintext", 8 * 100)
+ ct = cipher.encrypt(pt)
+
+ cipher = DES3.new(self.key_192, DES3.MODE_EAX, nonce=self.nonce_96)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_nonce(self):
+ # If not passed, the nonce is created randomly
+ cipher = AES.new(self.key_128, AES.MODE_EAX)
+ nonce1 = cipher.nonce
+ cipher = AES.new(self.key_128, AES.MODE_EAX)
+ nonce2 = cipher.nonce
+ self.assertEqual(len(nonce1), 16)
+ self.assertNotEqual(nonce1, nonce2)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, self.nonce_96)
+ ct = cipher.encrypt(self.data_128)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertEqual(ct, cipher.encrypt(self.data_128))
+
+ def test_nonce_must_be_bytes(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_EAX,
+ nonce=u'test12345678')
+
+ def test_nonce_length(self):
+ # nonce can be of any length (but not empty)
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_EAX,
+ nonce=b"")
+
+ for x in range(1, 128):
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=bchr(1) * x)
+ cipher.encrypt(bchr(1))
+
+ def test_block_size_128(self):
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertEqual(cipher.block_size, AES.block_size)
+
+ def test_block_size_64(self):
+ cipher = DES3.new(self.key_192, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertEqual(cipher.block_size, DES3.block_size)
+
+ def test_nonce_attribute(self):
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertEqual(cipher.nonce, self.nonce_96)
+
+ # By default, a 16 bytes long nonce is randomly generated
+ nonce1 = AES.new(self.key_128, AES.MODE_EAX).nonce
+ nonce2 = AES.new(self.key_128, AES.MODE_EAX).nonce
+ self.assertEqual(len(nonce1), 16)
+ self.assertNotEqual(nonce1, nonce2)
+
+ def test_unknown_parameters(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_EAX,
+ self.nonce_96, 7)
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96, unknown=7)
+
+ # But some are only known by the base cipher
+ # (e.g. use_aesni consumed by the AES module)
+ AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96,
+ use_aesni=False)
+
+ def test_null_encryption_decryption(self):
+ for func in "encrypt", "decrypt":
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ result = getattr(cipher, func)(b"")
+ self.assertEqual(result, b"")
+
+ def test_either_encrypt_or_decrypt(self):
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.encrypt(b"")
+ self.assertRaises(TypeError, cipher.decrypt, b"")
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.decrypt(b"")
+ self.assertRaises(TypeError, cipher.encrypt, b"")
+
+ def test_data_must_be_bytes(self):
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt, u'test1234567890-*')
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt, u'test1234567890-*')
+
+ def test_mac_len(self):
+ # Invalid MAC length
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96, mac_len=2-1)
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96, mac_len=16+1)
+
+ # Valid MAC length
+ for mac_len in range(2, 16 + 1):
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96,
+ mac_len=mac_len)
+ _, mac = cipher.encrypt_and_digest(self.data_128)
+ self.assertEqual(len(mac), mac_len)
+
+ # Default MAC length
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ _, mac = cipher.encrypt_and_digest(self.data_128)
+ self.assertEqual(len(mac), 16)
+
+ def test_invalid_mac(self):
+ from Crypto.Util.strxor import strxor_c
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ ct, mac = cipher.encrypt_and_digest(self.data_128)
+
+ invalid_mac = strxor_c(mac, 0x01)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct,
+ invalid_mac)
+
+ def test_hex_mac(self):
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ mac_hex = cipher.hexdigest()
+ self.assertEqual(cipher.digest(), unhexlify(mac_hex))
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.hexverify(mac_hex)
+
+ def test_message_chunks(self):
+ # Validate that both associated data and plaintext/ciphertext
+ # can be broken up in chunks of arbitrary length
+
+ auth_data = get_tag_random("authenticated data", 127)
+ plaintext = get_tag_random("plaintext", 127)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.update(auth_data)
+ ciphertext, ref_mac = cipher.encrypt_and_digest(plaintext)
+
+ def break_up(data, chunk_length):
+ return [data[i:i+chunk_length] for i in range(0, len(data),
+ chunk_length)]
+
+ # Encryption
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+
+ for chunk in break_up(auth_data, chunk_length):
+ cipher.update(chunk)
+ pt2 = b""
+ for chunk in break_up(ciphertext, chunk_length):
+ pt2 += cipher.decrypt(chunk)
+ self.assertEqual(plaintext, pt2)
+ cipher.verify(ref_mac)
+
+ # Decryption
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+
+ for chunk in break_up(auth_data, chunk_length):
+ cipher.update(chunk)
+ ct2 = b""
+ for chunk in break_up(plaintext, chunk_length):
+ ct2 += cipher.encrypt(chunk)
+ self.assertEqual(ciphertext, ct2)
+ self.assertEqual(cipher.digest(), ref_mac)
+
+ def test_bytearray(self):
+
+ # Encrypt
+ key_ba = bytearray(self.key_128)
+ nonce_ba = bytearray(self.nonce_96)
+ header_ba = bytearray(self.data_128)
+ data_ba = bytearray(self.data_128)
+
+ cipher1 = AES.new(self.key_128,
+ AES.MODE_EAX,
+ nonce=self.nonce_96)
+ cipher1.update(self.data_128)
+ ct = cipher1.encrypt(self.data_128)
+ tag = cipher1.digest()
+
+ cipher2 = AES.new(key_ba,
+ AES.MODE_EAX,
+ nonce=nonce_ba)
+ key_ba[:3] = b'\xFF\xFF\xFF'
+ nonce_ba[:3] = b'\xFF\xFF\xFF'
+ cipher2.update(header_ba)
+ header_ba[:3] = b'\xFF\xFF\xFF'
+ ct_test = cipher2.encrypt(data_ba)
+ data_ba[:3] = b'\x99\x99\x99'
+ tag_test = cipher2.digest()
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key_ba = bytearray(self.key_128)
+ nonce_ba = bytearray(self.nonce_96)
+ header_ba = bytearray(self.data_128)
+ ct_ba = bytearray(ct)
+ tag_ba = bytearray(tag)
+ del data_ba
+
+ cipher3 = AES.new(key_ba,
+ AES.MODE_EAX,
+ nonce=nonce_ba)
+ key_ba[:3] = b'\xFF\xFF\xFF'
+ nonce_ba[:3] = b'\xFF\xFF\xFF'
+ cipher3.update(header_ba)
+ header_ba[:3] = b'\xFF\xFF\xFF'
+ pt_test = cipher3.decrypt(ct_ba)
+ ct_ba[:3] = b'\xFF\xFF\xFF'
+ cipher3.verify(tag_ba)
+
+ self.assertEqual(pt_test, self.data_128)
+
+ def test_memoryview(self):
+
+ # Encrypt
+ key_mv = memoryview(bytearray(self.key_128))
+ nonce_mv = memoryview(bytearray(self.nonce_96))
+ header_mv = memoryview(bytearray(self.data_128))
+ data_mv = memoryview(bytearray(self.data_128))
+
+ cipher1 = AES.new(self.key_128,
+ AES.MODE_EAX,
+ nonce=self.nonce_96)
+ cipher1.update(self.data_128)
+ ct = cipher1.encrypt(self.data_128)
+ tag = cipher1.digest()
+
+ cipher2 = AES.new(key_mv,
+ AES.MODE_EAX,
+ nonce=nonce_mv)
+ key_mv[:3] = b'\xFF\xFF\xFF'
+ nonce_mv[:3] = b'\xFF\xFF\xFF'
+ cipher2.update(header_mv)
+ header_mv[:3] = b'\xFF\xFF\xFF'
+ ct_test = cipher2.encrypt(data_mv)
+ data_mv[:3] = b'\x99\x99\x99'
+ tag_test = cipher2.digest()
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key_mv = memoryview(bytearray(self.key_128))
+ nonce_mv = memoryview(bytearray(self.nonce_96))
+ header_mv = memoryview(bytearray(self.data_128))
+ ct_mv = memoryview(bytearray(ct))
+ tag_mv = memoryview(bytearray(tag))
+ del data_mv
+
+ cipher3 = AES.new(key_mv,
+ AES.MODE_EAX,
+ nonce=nonce_mv)
+ key_mv[:3] = b'\xFF\xFF\xFF'
+ nonce_mv[:3] = b'\xFF\xFF\xFF'
+ cipher3.update(header_mv)
+ header_mv[:3] = b'\xFF\xFF\xFF'
+ pt_test = cipher3.decrypt(ct_mv)
+ ct_mv[:3] = b'\x99\x99\x99'
+ cipher3.verify(tag_mv)
+
+ self.assertEqual(pt_test, self.data_128)
+
+ def test_output_param(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ ct = cipher.encrypt(pt)
+ tag = cipher.digest()
+
+ output = bytearray(128)
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ res, tag_out = cipher.encrypt_and_digest(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+ self.assertEqual(tag, tag_out)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ res = cipher.decrypt_and_verify(ct, tag, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ def test_output_param_memoryview(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ ct = cipher.encrypt(pt)
+
+ output = memoryview(bytearray(128))
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ def test_output_param_neg(self):
+ LEN_PT = 16
+
+ pt = b'5' * LEN_PT
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ ct = cipher.encrypt(pt)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0' * LEN_PT)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0' * LEN_PT)
+
+ shorter_output = bytearray(LEN_PT - 1)
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+class EaxFSMTests(unittest.TestCase):
+
+ key_128 = get_tag_random("key_128", 16)
+ nonce_96 = get_tag_random("nonce_128", 12)
+ data_128 = get_tag_random("data_128", 16)
+
+ def test_valid_init_encrypt_decrypt_digest_verify(self):
+ # No authenticated data, fixed plaintext
+ # Verify path INIT->ENCRYPT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data_128)
+ mac = cipher.digest()
+
+ # Verify path INIT->DECRYPT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+
+ def test_valid_init_update_digest_verify(self):
+ # No plaintext, fixed authenticated data
+ # Verify path INIT->UPDATE->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ cipher.verify(mac)
+
+ def test_valid_full_path(self):
+ # Fixed authenticated data, fixed plaintext
+ # Verify path INIT->UPDATE->ENCRYPT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ ct = cipher.encrypt(self.data_128)
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->DECRYPT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+
+ def test_valid_init_digest(self):
+ # Verify path INIT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.digest()
+
+ def test_valid_init_verify(self):
+ # Verify path INIT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ mac = cipher.digest()
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.verify(mac)
+
+ def test_valid_multiple_encrypt_or_decrypt(self):
+ for method_name in "encrypt", "decrypt":
+ for auth_data in (None, b"333", self.data_128,
+ self.data_128 + b"3"):
+ if auth_data is None:
+ assoc_len = None
+ else:
+ assoc_len = len(auth_data)
+ cipher = AES.new(self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96)
+ if auth_data is not None:
+ cipher.update(auth_data)
+ method = getattr(cipher, method_name)
+ method(self.data_128)
+ method(self.data_128)
+ method(self.data_128)
+ method(self.data_128)
+
+ def test_valid_multiple_digest_or_verify(self):
+ # Multiple calls to digest
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ first_mac = cipher.digest()
+ for x in range(4):
+ self.assertEqual(first_mac, cipher.digest())
+
+ # Multiple calls to verify
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ for x in range(5):
+ cipher.verify(first_mac)
+
+ def test_valid_encrypt_and_digest_decrypt_and_verify(self):
+ # encrypt_and_digest
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ ct, mac = cipher.encrypt_and_digest(self.data_128)
+
+ # decrypt_and_verify
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.update(self.data_128)
+ pt = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(self.data_128, pt)
+
+ def test_invalid_mixing_encrypt_decrypt(self):
+ # Once per method, with or without assoc. data
+ for method1_name, method2_name in (("encrypt", "decrypt"),
+ ("decrypt", "encrypt")):
+ for assoc_data_present in (True, False):
+ cipher = AES.new(self.key_128, AES.MODE_EAX,
+ nonce=self.nonce_96)
+ if assoc_data_present:
+ cipher.update(self.data_128)
+ getattr(cipher, method1_name)(self.data_128)
+ self.assertRaises(TypeError, getattr(cipher, method2_name),
+ self.data_128)
+
+ def test_invalid_encrypt_or_update_after_digest(self):
+ for method_name in "encrypt", "update":
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.encrypt(self.data_128)
+ cipher.digest()
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data_128)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.encrypt_and_digest(self.data_128)
+
+ def test_invalid_decrypt_or_update_after_verify(self):
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data_128)
+ mac = cipher.digest()
+
+ for method_name in "decrypt", "update":
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data_128)
+
+ cipher = AES.new(self.key_128, AES.MODE_EAX, nonce=self.nonce_96)
+ cipher.decrypt_and_verify(ct, mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data_128)
+
+
+class TestVectorsPaper(unittest.TestCase):
+ """Class exercising the EAX test vectors found in
+ http://www.cs.ucdavis.edu/~rogaway/papers/eax.pdf"""
+
+ test_vectors_hex = [
+ ( '6bfb914fd07eae6b',
+ '',
+ '',
+ 'e037830e8389f27b025a2d6527e79d01',
+ '233952dee4d5ed5f9b9c6d6ff80ff478',
+ '62EC67F9C3A4A407FCB2A8C49031A8B3'
+ ),
+ (
+ 'fa3bfd4806eb53fa',
+ 'f7fb',
+ '19dd',
+ '5c4c9331049d0bdab0277408f67967e5',
+ '91945d3f4dcbee0bf45ef52255f095a4',
+ 'BECAF043B0A23D843194BA972C66DEBD'
+ ),
+ ( '234a3463c1264ac6',
+ '1a47cb4933',
+ 'd851d5bae0',
+ '3a59f238a23e39199dc9266626c40f80',
+ '01f74ad64077f2e704c0f60ada3dd523',
+ '70C3DB4F0D26368400A10ED05D2BFF5E'
+ ),
+ (
+ '33cce2eabff5a79d',
+ '481c9e39b1',
+ '632a9d131a',
+ 'd4c168a4225d8e1ff755939974a7bede',
+ 'd07cf6cbb7f313bdde66b727afd3c5e8',
+ '8408DFFF3C1A2B1292DC199E46B7D617'
+ ),
+ (
+ 'aeb96eaebe2970e9',
+ '40d0c07da5e4',
+ '071dfe16c675',
+ 'cb0677e536f73afe6a14b74ee49844dd',
+ '35b6d0580005bbc12b0587124557d2c2',
+ 'FDB6B06676EEDC5C61D74276E1F8E816'
+ ),
+ (
+ 'd4482d1ca78dce0f',
+ '4de3b35c3fc039245bd1fb7d',
+ '835bb4f15d743e350e728414',
+ 'abb8644fd6ccb86947c5e10590210a4f',
+ 'bd8e6e11475e60b268784c38c62feb22',
+ '6EAC5C93072D8E8513F750935E46DA1B'
+ ),
+ (
+ '65d2017990d62528',
+ '8b0a79306c9ce7ed99dae4f87f8dd61636',
+ '02083e3979da014812f59f11d52630da30',
+ '137327d10649b0aa6e1c181db617d7f2',
+ '7c77d6e813bed5ac98baa417477a2e7d',
+ '1A8C98DCD73D38393B2BF1569DEEFC19'
+ ),
+ (
+ '54b9f04e6a09189a',
+ '1bda122bce8a8dbaf1877d962b8592dd2d56',
+ '2ec47b2c4954a489afc7ba4897edcdae8cc3',
+ '3b60450599bd02c96382902aef7f832a',
+ '5fff20cafab119ca2fc73549e20f5b0d',
+ 'DDE59B97D722156D4D9AFF2BC7559826'
+ ),
+ (
+ '899a175897561d7e',
+ '6cf36720872b8513f6eab1a8a44438d5ef11',
+ '0de18fd0fdd91e7af19f1d8ee8733938b1e8',
+ 'e7f6d2231618102fdb7fe55ff1991700',
+ 'a4a4782bcffd3ec5e7ef6d8c34a56123',
+ 'B781FCF2F75FA5A8DE97A9CA48E522EC'
+ ),
+ (
+ '126735fcc320d25a',
+ 'ca40d7446e545ffaed3bd12a740a659ffbbb3ceab7',
+ 'cb8920f87a6c75cff39627b56e3ed197c552d295a7',
+ 'cfc46afc253b4652b1af3795b124ab6e',
+ '8395fcf1e95bebd697bd010bc766aac3',
+ '22E7ADD93CFC6393C57EC0B3C17D6B44'
+ ),
+ ]
+
+ test_vectors = [[unhexlify(x) for x in tv] for tv in test_vectors_hex]
+
+ def runTest(self):
+ for assoc_data, pt, ct, mac, key, nonce in self.test_vectors:
+ # Encrypt
+ cipher = AES.new(key, AES.MODE_EAX, nonce, mac_len=len(mac))
+ cipher.update(assoc_data)
+ ct2, mac2 = cipher.encrypt_and_digest(pt)
+ self.assertEqual(ct, ct2)
+ self.assertEqual(mac, mac2)
+
+ # Decrypt
+ cipher = AES.new(key, AES.MODE_EAX, nonce, mac_len=len(mac))
+ cipher.update(assoc_data)
+ pt2 = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(pt, pt2)
+
+
+class TestVectorsWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._id = "None"
+
+ def setUp(self):
+
+ def filter_tag(group):
+ return group['tagSize'] // 8
+
+ self.tv = load_test_vectors_wycheproof(("Cipher", "wycheproof"),
+ "aes_eax_test.json",
+ "Wycheproof EAX",
+ group_tag={'tag_size': filter_tag})
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_encrypt(self, tv):
+ self._id = "Wycheproof Encrypt EAX Test #" + str(tv.id)
+
+ try:
+ cipher = AES.new(tv.key, AES.MODE_EAX, tv.iv, mac_len=tv.tag_size)
+ except ValueError as e:
+ assert len(tv.iv) == 0 and "Nonce cannot be empty" in str(e)
+ return
+
+ cipher.update(tv.aad)
+ ct, tag = cipher.encrypt_and_digest(tv.msg)
+ if tv.valid:
+ self.assertEqual(ct, tv.ct)
+ self.assertEqual(tag, tv.tag)
+ self.warn(tv)
+
+ def test_decrypt(self, tv):
+ self._id = "Wycheproof Decrypt EAX Test #" + str(tv.id)
+
+ try:
+ cipher = AES.new(tv.key, AES.MODE_EAX, tv.iv, mac_len=tv.tag_size)
+ except ValueError as e:
+ assert len(tv.iv) == 0 and "Nonce cannot be empty" in str(e)
+ return
+
+ cipher.update(tv.aad)
+ try:
+ pt = cipher.decrypt_and_verify(tv.ct, tv.tag)
+ except ValueError:
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.assertEqual(pt, tv.msg)
+ self.warn(tv)
+
+ def test_corrupt_decrypt(self, tv):
+ self._id = "Wycheproof Corrupt Decrypt EAX Test #" + str(tv.id)
+ if len(tv.iv) == 0 or len(tv.ct) < 1:
+ return
+ cipher = AES.new(tv.key, AES.MODE_EAX, tv.iv, mac_len=tv.tag_size)
+ cipher.update(tv.aad)
+ ct_corrupt = strxor(tv.ct, b"\x00" * (len(tv.ct) - 1) + b"\x01")
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct_corrupt, tv.tag)
+
+ def runTest(self):
+
+ for tv in self.tv:
+ self.test_encrypt(tv)
+ self.test_decrypt(tv)
+ self.test_corrupt_decrypt(tv)
+
+
+class TestOtherCiphers(unittest.TestCase):
+
+ @classmethod
+ def create_test(cls, name, factory, key_size):
+
+ def test_template(self, factory=factory, key_size=key_size):
+ cipher = factory.new(get_tag_random("cipher", key_size),
+ factory.MODE_EAX,
+ nonce=b"nonce")
+ ct, mac = cipher.encrypt_and_digest(b"plaintext")
+
+ cipher = factory.new(get_tag_random("cipher", key_size),
+ factory.MODE_EAX,
+ nonce=b"nonce")
+ pt2 = cipher.decrypt_and_verify(ct, mac)
+
+ self.assertEqual(b"plaintext", pt2)
+
+ setattr(cls, "test_" + name, test_template)
+
+
+from Crypto.Cipher import DES, DES3, ARC2, CAST, Blowfish
+
+TestOtherCiphers.create_test("DES_" + str(DES.key_size), DES, DES.key_size)
+for ks in DES3.key_size:
+ TestOtherCiphers.create_test("DES3_" + str(ks), DES3, ks)
+for ks in ARC2.key_size:
+ TestOtherCiphers.create_test("ARC2_" + str(ks), ARC2, ks)
+for ks in CAST.key_size:
+ TestOtherCiphers.create_test("CAST_" + str(ks), CAST, ks)
+for ks in Blowfish.key_size:
+ TestOtherCiphers.create_test("Blowfish_" + str(ks), Blowfish, ks)
+
+
+def get_tests(config={}):
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ tests = []
+ tests += list_test_cases(EaxTests)
+ tests += list_test_cases(EaxFSMTests)
+ tests += [ TestVectorsPaper() ]
+ tests += [ TestVectorsWycheproof(wycheproof_warnings) ]
+ tests += list_test_cases(TestOtherCiphers)
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_GCM.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_GCM.py
new file mode 100644
index 00000000..dd8da2fd
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_GCM.py
@@ -0,0 +1,951 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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 __future__ import print_function
+
+import unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors, load_test_vectors_wycheproof
+
+from Crypto.Util.py3compat import tobytes, bchr
+from Crypto.Cipher import AES
+from Crypto.Hash import SHAKE128, SHA256
+
+from Crypto.Util.strxor import strxor
+
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+
+class GcmTests(unittest.TestCase):
+
+ key_128 = get_tag_random("key_128", 16)
+ nonce_96 = get_tag_random("nonce_128", 12)
+ data = get_tag_random("data", 128)
+
+ def test_loopback_128(self):
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ pt = get_tag_random("plaintext", 16 * 100)
+ ct = cipher.encrypt(pt)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_nonce(self):
+ # Nonce is optional (a random one will be created)
+ AES.new(self.key_128, AES.MODE_GCM)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, self.nonce_96)
+ ct = cipher.encrypt(self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ self.assertEqual(ct, cipher.encrypt(self.data))
+
+ def test_nonce_must_be_bytes(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_GCM,
+ nonce=u'test12345678')
+
+ def test_nonce_length(self):
+ # nonce can be of any length (but not empty)
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_GCM,
+ nonce=b"")
+
+ for x in range(1, 128):
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=bchr(1) * x)
+ cipher.encrypt(bchr(1))
+
+ def test_block_size_128(self):
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ self.assertEqual(cipher.block_size, AES.block_size)
+
+ def test_nonce_attribute(self):
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ self.assertEqual(cipher.nonce, self.nonce_96)
+
+ # By default, a 15 bytes long nonce is randomly generated
+ nonce1 = AES.new(self.key_128, AES.MODE_GCM).nonce
+ nonce2 = AES.new(self.key_128, AES.MODE_GCM).nonce
+ self.assertEqual(len(nonce1), 16)
+ self.assertNotEqual(nonce1, nonce2)
+
+ def test_unknown_parameters(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_GCM,
+ self.nonce_96, 7)
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96, unknown=7)
+
+ # But some are only known by the base cipher
+ # (e.g. use_aesni consumed by the AES module)
+ AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96,
+ use_aesni=False)
+
+ def test_null_encryption_decryption(self):
+ for func in "encrypt", "decrypt":
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ result = getattr(cipher, func)(b"")
+ self.assertEqual(result, b"")
+
+ def test_either_encrypt_or_decrypt(self):
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.encrypt(b"")
+ self.assertRaises(TypeError, cipher.decrypt, b"")
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.decrypt(b"")
+ self.assertRaises(TypeError, cipher.encrypt, b"")
+
+ def test_data_must_be_bytes(self):
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt, u'test1234567890-*')
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt, u'test1234567890-*')
+
+ def test_mac_len(self):
+ # Invalid MAC length
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96, mac_len=3)
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96, mac_len=16+1)
+
+ # Valid MAC length
+ for mac_len in range(5, 16 + 1):
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96,
+ mac_len=mac_len)
+ _, mac = cipher.encrypt_and_digest(self.data)
+ self.assertEqual(len(mac), mac_len)
+
+ # Default MAC length
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ _, mac = cipher.encrypt_and_digest(self.data)
+ self.assertEqual(len(mac), 16)
+
+ def test_invalid_mac(self):
+ from Crypto.Util.strxor import strxor_c
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ ct, mac = cipher.encrypt_and_digest(self.data)
+
+ invalid_mac = strxor_c(mac, 0x01)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct,
+ invalid_mac)
+
+ def test_hex_mac(self):
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ mac_hex = cipher.hexdigest()
+ self.assertEqual(cipher.digest(), unhexlify(mac_hex))
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.hexverify(mac_hex)
+
+ def test_message_chunks(self):
+ # Validate that both associated data and plaintext/ciphertext
+ # can be broken up in chunks of arbitrary length
+
+ auth_data = get_tag_random("authenticated data", 127)
+ plaintext = get_tag_random("plaintext", 127)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.update(auth_data)
+ ciphertext, ref_mac = cipher.encrypt_and_digest(plaintext)
+
+ def break_up(data, chunk_length):
+ return [data[i:i+chunk_length] for i in range(0, len(data),
+ chunk_length)]
+
+ # Encryption
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+
+ for chunk in break_up(auth_data, chunk_length):
+ cipher.update(chunk)
+ pt2 = b""
+ for chunk in break_up(ciphertext, chunk_length):
+ pt2 += cipher.decrypt(chunk)
+ self.assertEqual(plaintext, pt2)
+ cipher.verify(ref_mac)
+
+ # Decryption
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+
+ for chunk in break_up(auth_data, chunk_length):
+ cipher.update(chunk)
+ ct2 = b""
+ for chunk in break_up(plaintext, chunk_length):
+ ct2 += cipher.encrypt(chunk)
+ self.assertEqual(ciphertext, ct2)
+ self.assertEqual(cipher.digest(), ref_mac)
+
+ def test_bytearray(self):
+
+ # Encrypt
+ key_ba = bytearray(self.key_128)
+ nonce_ba = bytearray(self.nonce_96)
+ header_ba = bytearray(self.data)
+ data_ba = bytearray(self.data)
+
+ cipher1 = AES.new(self.key_128,
+ AES.MODE_GCM,
+ nonce=self.nonce_96)
+ cipher1.update(self.data)
+ ct = cipher1.encrypt(self.data)
+ tag = cipher1.digest()
+
+ cipher2 = AES.new(key_ba,
+ AES.MODE_GCM,
+ nonce=nonce_ba)
+ key_ba[:3] = b"\xFF\xFF\xFF"
+ nonce_ba[:3] = b"\xFF\xFF\xFF"
+ cipher2.update(header_ba)
+ header_ba[:3] = b"\xFF\xFF\xFF"
+ ct_test = cipher2.encrypt(data_ba)
+ data_ba[:3] = b"\xFF\xFF\xFF"
+ tag_test = cipher2.digest()
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key_ba = bytearray(self.key_128)
+ nonce_ba = bytearray(self.nonce_96)
+ header_ba = bytearray(self.data)
+ del data_ba
+
+ cipher4 = AES.new(key_ba,
+ AES.MODE_GCM,
+ nonce=nonce_ba)
+ key_ba[:3] = b"\xFF\xFF\xFF"
+ nonce_ba[:3] = b"\xFF\xFF\xFF"
+ cipher4.update(header_ba)
+ header_ba[:3] = b"\xFF\xFF\xFF"
+ pt_test = cipher4.decrypt_and_verify(bytearray(ct_test), bytearray(tag_test))
+
+ self.assertEqual(self.data, pt_test)
+
+ def test_memoryview(self):
+
+ # Encrypt
+ key_mv = memoryview(bytearray(self.key_128))
+ nonce_mv = memoryview(bytearray(self.nonce_96))
+ header_mv = memoryview(bytearray(self.data))
+ data_mv = memoryview(bytearray(self.data))
+
+ cipher1 = AES.new(self.key_128,
+ AES.MODE_GCM,
+ nonce=self.nonce_96)
+ cipher1.update(self.data)
+ ct = cipher1.encrypt(self.data)
+ tag = cipher1.digest()
+
+ cipher2 = AES.new(key_mv,
+ AES.MODE_GCM,
+ nonce=nonce_mv)
+ key_mv[:3] = b"\xFF\xFF\xFF"
+ nonce_mv[:3] = b"\xFF\xFF\xFF"
+ cipher2.update(header_mv)
+ header_mv[:3] = b"\xFF\xFF\xFF"
+ ct_test = cipher2.encrypt(data_mv)
+ data_mv[:3] = b"\xFF\xFF\xFF"
+ tag_test = cipher2.digest()
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key_mv = memoryview(bytearray(self.key_128))
+ nonce_mv = memoryview(bytearray(self.nonce_96))
+ header_mv = memoryview(bytearray(self.data))
+ del data_mv
+
+ cipher4 = AES.new(key_mv,
+ AES.MODE_GCM,
+ nonce=nonce_mv)
+ key_mv[:3] = b"\xFF\xFF\xFF"
+ nonce_mv[:3] = b"\xFF\xFF\xFF"
+ cipher4.update(header_mv)
+ header_mv[:3] = b"\xFF\xFF\xFF"
+ pt_test = cipher4.decrypt_and_verify(memoryview(ct_test), memoryview(tag_test))
+
+ self.assertEqual(self.data, pt_test)
+
+ def test_output_param(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ ct = cipher.encrypt(pt)
+ tag = cipher.digest()
+
+ output = bytearray(128)
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ res, tag_out = cipher.encrypt_and_digest(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+ self.assertEqual(tag, tag_out)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ res = cipher.decrypt_and_verify(ct, tag, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ def test_output_param_memoryview(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ ct = cipher.encrypt(pt)
+
+ output = memoryview(bytearray(128))
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ def test_output_param_neg(self):
+ LEN_PT = 128
+
+ pt = b'5' * LEN_PT
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ ct = cipher.encrypt(pt)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0' * LEN_PT)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0' * LEN_PT)
+
+ shorter_output = bytearray(LEN_PT - 1)
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+class GcmFSMTests(unittest.TestCase):
+
+ key_128 = get_tag_random("key_128", 16)
+ nonce_96 = get_tag_random("nonce_128", 12)
+ data = get_tag_random("data", 128)
+
+ def test_valid_init_encrypt_decrypt_digest_verify(self):
+ # No authenticated data, fixed plaintext
+ # Verify path INIT->ENCRYPT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data)
+ mac = cipher.digest()
+
+ # Verify path INIT->DECRYPT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+
+ def test_valid_init_update_digest_verify(self):
+ # No plaintext, fixed authenticated data
+ # Verify path INIT->UPDATE->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ cipher.verify(mac)
+
+ def test_valid_full_path(self):
+ # Fixed authenticated data, fixed plaintext
+ # Verify path INIT->UPDATE->ENCRYPT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ ct = cipher.encrypt(self.data)
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->DECRYPT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+
+ def test_valid_init_digest(self):
+ # Verify path INIT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.digest()
+
+ def test_valid_init_verify(self):
+ # Verify path INIT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ mac = cipher.digest()
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.verify(mac)
+
+ def test_valid_multiple_encrypt_or_decrypt(self):
+ for method_name in "encrypt", "decrypt":
+ for auth_data in (None, b"333", self.data,
+ self.data + b"3"):
+ if auth_data is None:
+ assoc_len = None
+ else:
+ assoc_len = len(auth_data)
+ cipher = AES.new(self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96)
+ if auth_data is not None:
+ cipher.update(auth_data)
+ method = getattr(cipher, method_name)
+ method(self.data)
+ method(self.data)
+ method(self.data)
+ method(self.data)
+
+ def test_valid_multiple_digest_or_verify(self):
+ # Multiple calls to digest
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.update(self.data)
+ first_mac = cipher.digest()
+ for x in range(4):
+ self.assertEqual(first_mac, cipher.digest())
+
+ # Multiple calls to verify
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.update(self.data)
+ for x in range(5):
+ cipher.verify(first_mac)
+
+ def test_valid_encrypt_and_digest_decrypt_and_verify(self):
+ # encrypt_and_digest
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.update(self.data)
+ ct, mac = cipher.encrypt_and_digest(self.data)
+
+ # decrypt_and_verify
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.update(self.data)
+ pt = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(self.data, pt)
+
+ def test_invalid_mixing_encrypt_decrypt(self):
+ # Once per method, with or without assoc. data
+ for method1_name, method2_name in (("encrypt", "decrypt"),
+ ("decrypt", "encrypt")):
+ for assoc_data_present in (True, False):
+ cipher = AES.new(self.key_128, AES.MODE_GCM,
+ nonce=self.nonce_96)
+ if assoc_data_present:
+ cipher.update(self.data)
+ getattr(cipher, method1_name)(self.data)
+ self.assertRaises(TypeError, getattr(cipher, method2_name),
+ self.data)
+
+ def test_invalid_encrypt_or_update_after_digest(self):
+ for method_name in "encrypt", "update":
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.encrypt(self.data)
+ cipher.digest()
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.encrypt_and_digest(self.data)
+
+ def test_invalid_decrypt_or_update_after_verify(self):
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data)
+ mac = cipher.digest()
+
+ for method_name in "decrypt", "update":
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ cipher.verify(mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_GCM, nonce=self.nonce_96)
+ cipher.decrypt_and_verify(ct, mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data)
+
+
+class TestVectors(unittest.TestCase):
+ """Class exercising the GCM test vectors found in
+ http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf"""
+
+ # List of test vectors, each made up of:
+ # - authenticated data
+ # - plaintext
+ # - ciphertext
+ # - MAC
+ # - AES key
+ # - nonce
+ test_vectors_hex = [
+ (
+ '',
+ '',
+ '',
+ '58e2fccefa7e3061367f1d57a4e7455a',
+ '00000000000000000000000000000000',
+ '000000000000000000000000'
+ ),
+ (
+ '',
+ '00000000000000000000000000000000',
+ '0388dace60b6a392f328c2b971b2fe78',
+ 'ab6e47d42cec13bdf53a67b21257bddf',
+ '00000000000000000000000000000000',
+ '000000000000000000000000'
+ ),
+ (
+ '',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255',
+ '42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e' +
+ '21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f5985',
+ '4d5c2af327cd64a62cf35abd2ba6fab4',
+ 'feffe9928665731c6d6a8f9467308308',
+ 'cafebabefacedbaddecaf888'
+ ),
+ (
+ 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39',
+ '42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e' +
+ '21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091',
+ '5bc94fbc3221a5db94fae95ae7121a47',
+ 'feffe9928665731c6d6a8f9467308308',
+ 'cafebabefacedbaddecaf888'
+ ),
+ (
+ 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39',
+ '61353b4c2806934a777ff51fa22a4755699b2a714fcdc6f83766e5f97b6c7423' +
+ '73806900e49f24b22b097544d4896b424989b5e1ebac0f07c23f4598',
+ '3612d2e79e3b0785561be14aaca2fccb',
+ 'feffe9928665731c6d6a8f9467308308',
+ 'cafebabefacedbad'
+ ),
+ (
+ 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39',
+ '8ce24998625615b603a033aca13fb894be9112a5c3a211a8ba262a3cca7e2ca7' +
+ '01e4a9a4fba43c90ccdcb281d48c7c6fd62875d2aca417034c34aee5',
+ '619cc5aefffe0bfa462af43c1699d050',
+ 'feffe9928665731c6d6a8f9467308308',
+ '9313225df88406e555909c5aff5269aa' +
+ '6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b5254' +
+ '16aedbf5a0de6a57a637b39b'
+ ),
+ (
+ '',
+ '',
+ '',
+ 'cd33b28ac773f74ba00ed1f312572435',
+ '000000000000000000000000000000000000000000000000',
+ '000000000000000000000000'
+ ),
+ (
+ '',
+ '00000000000000000000000000000000',
+ '98e7247c07f0fe411c267e4384b0f600',
+ '2ff58d80033927ab8ef4d4587514f0fb',
+ '000000000000000000000000000000000000000000000000',
+ '000000000000000000000000'
+ ),
+ (
+ '',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255',
+ '3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c' +
+ '7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710acade256',
+ '9924a7c8587336bfb118024db8674a14',
+ 'feffe9928665731c6d6a8f9467308308feffe9928665731c',
+ 'cafebabefacedbaddecaf888'
+ ),
+ (
+ 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39',
+ '3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c' +
+ '7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710',
+ '2519498e80f1478f37ba55bd6d27618c',
+ 'feffe9928665731c6d6a8f9467308308feffe9928665731c',
+ 'cafebabefacedbaddecaf888'
+ ),
+ (
+ 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39',
+ '0f10f599ae14a154ed24b36e25324db8c566632ef2bbb34f8347280fc4507057' +
+ 'fddc29df9a471f75c66541d4d4dad1c9e93a19a58e8b473fa0f062f7',
+ '65dcc57fcf623a24094fcca40d3533f8',
+ 'feffe9928665731c6d6a8f9467308308feffe9928665731c',
+ 'cafebabefacedbad'
+ ),
+ (
+ 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39',
+ 'd27e88681ce3243c4830165a8fdcf9ff1de9a1d8e6b447ef6ef7b79828666e45' +
+ '81e79012af34ddd9e2f037589b292db3e67c036745fa22e7e9b7373b',
+ 'dcf566ff291c25bbb8568fc3d376a6d9',
+ 'feffe9928665731c6d6a8f9467308308feffe9928665731c',
+ '9313225df88406e555909c5aff5269aa' +
+ '6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b5254' +
+ '16aedbf5a0de6a57a637b39b'
+ ),
+ (
+ '',
+ '',
+ '',
+ '530f8afbc74536b9a963b4f1c4cb738b',
+ '0000000000000000000000000000000000000000000000000000000000000000',
+ '000000000000000000000000'
+ ),
+ (
+ '',
+ '00000000000000000000000000000000',
+ 'cea7403d4d606b6e074ec5d3baf39d18',
+ 'd0d1c8a799996bf0265b98b5d48ab919',
+ '0000000000000000000000000000000000000000000000000000000000000000',
+ '000000000000000000000000'
+ ),
+ ( '',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255',
+ '522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa' +
+ '8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad',
+ 'b094dac5d93471bdec1a502270e3cc6c',
+ 'feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308',
+ 'cafebabefacedbaddecaf888'
+ ),
+ (
+ 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39',
+ '522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa' +
+ '8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662',
+ '76fc6ece0f4e1768cddf8853bb2d551b',
+ 'feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308',
+ 'cafebabefacedbaddecaf888'
+ ),
+ (
+ 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39',
+ 'c3762df1ca787d32ae47c13bf19844cbaf1ae14d0b976afac52ff7d79bba9de0' +
+ 'feb582d33934a4f0954cc2363bc73f7862ac430e64abe499f47c9b1f',
+ '3a337dbf46a792c45e454913fe2ea8f2',
+ 'feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308',
+ 'cafebabefacedbad'
+ ),
+ (
+ 'feedfacedeadbeeffeedfacedeadbeefabaddad2',
+ 'd9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72' +
+ '1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39',
+ '5a8def2f0c9e53f1f75d7853659e2a20eeb2b22aafde6419a058ab4f6f746bf4' +
+ '0fc0c3b780f244452da3ebf1c5d82cdea2418997200ef82e44ae7e3f',
+ 'a44a8266ee1c8eb0c8b5d4cf5ae9f19a',
+ 'feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308',
+ '9313225df88406e555909c5aff5269aa' +
+ '6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b5254' +
+ '16aedbf5a0de6a57a637b39b'
+ )
+ ]
+
+ test_vectors = [[unhexlify(x) for x in tv] for tv in test_vectors_hex]
+
+ def runTest(self):
+ for assoc_data, pt, ct, mac, key, nonce in self.test_vectors:
+
+ # Encrypt
+ cipher = AES.new(key, AES.MODE_GCM, nonce, mac_len=len(mac))
+ cipher.update(assoc_data)
+ ct2, mac2 = cipher.encrypt_and_digest(pt)
+ self.assertEqual(ct, ct2)
+ self.assertEqual(mac, mac2)
+
+ # Decrypt
+ cipher = AES.new(key, AES.MODE_GCM, nonce, mac_len=len(mac))
+ cipher.update(assoc_data)
+ pt2 = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(pt, pt2)
+
+
+class TestVectorsGueronKrasnov(unittest.TestCase):
+ """Class exercising the GCM test vectors found in
+ 'The fragility of AES-GCM authentication algorithm', Gueron, Krasnov
+ https://eprint.iacr.org/2013/157.pdf"""
+
+ def test_1(self):
+ key = unhexlify("3da6c536d6295579c0959a7043efb503")
+ iv = unhexlify("2b926197d34e091ef722db94")
+ aad = unhexlify("00000000000000000000000000000000" +
+ "000102030405060708090a0b0c0d0e0f" +
+ "101112131415161718191a1b1c1d1e1f" +
+ "202122232425262728292a2b2c2d2e2f" +
+ "303132333435363738393a3b3c3d3e3f")
+ digest = unhexlify("69dd586555ce3fcc89663801a71d957b")
+
+ cipher = AES.new(key, AES.MODE_GCM, iv).update(aad)
+ self.assertEqual(digest, cipher.digest())
+
+ def test_2(self):
+ key = unhexlify("843ffcf5d2b72694d19ed01d01249412")
+ iv = unhexlify("dbcca32ebf9b804617c3aa9e")
+ aad = unhexlify("00000000000000000000000000000000" +
+ "101112131415161718191a1b1c1d1e1f")
+ pt = unhexlify("000102030405060708090a0b0c0d0e0f" +
+ "101112131415161718191a1b1c1d1e1f" +
+ "202122232425262728292a2b2c2d2e2f" +
+ "303132333435363738393a3b3c3d3e3f" +
+ "404142434445464748494a4b4c4d4e4f")
+ ct = unhexlify("6268c6fa2a80b2d137467f092f657ac0" +
+ "4d89be2beaa623d61b5a868c8f03ff95" +
+ "d3dcee23ad2f1ab3a6c80eaf4b140eb0" +
+ "5de3457f0fbc111a6b43d0763aa422a3" +
+ "013cf1dc37fe417d1fbfc449b75d4cc5")
+ digest = unhexlify("3b629ccfbc1119b7319e1dce2cd6fd6d")
+
+ cipher = AES.new(key, AES.MODE_GCM, iv).update(aad)
+ ct2, digest2 = cipher.encrypt_and_digest(pt)
+
+ self.assertEqual(ct, ct2)
+ self.assertEqual(digest, digest2)
+
+
+class NISTTestVectorsGCM(unittest.TestCase):
+
+ def __init__(self, a):
+ self.use_clmul = True
+ unittest.TestCase.__init__(self, a)
+
+
+class NISTTestVectorsGCM_no_clmul(unittest.TestCase):
+
+ def __init__(self, a):
+ self.use_clmul = False
+ unittest.TestCase.__init__(self, a)
+
+
+test_vectors_nist = load_test_vectors(
+ ("Cipher", "AES"),
+ "gcmDecrypt128.rsp",
+ "GCM decrypt",
+ {"count": lambda x: int(x)}) or []
+
+test_vectors_nist += load_test_vectors(
+ ("Cipher", "AES"),
+ "gcmEncryptExtIV128.rsp",
+ "GCM encrypt",
+ {"count": lambda x: int(x)}) or []
+
+for idx, tv in enumerate(test_vectors_nist):
+
+ # The test vector file contains some directive lines
+ if isinstance(tv, str):
+ continue
+
+ def single_test(self, tv=tv):
+
+ self.description = tv.desc
+ cipher = AES.new(tv.key, AES.MODE_GCM, nonce=tv.iv,
+ mac_len=len(tv.tag), use_clmul=self.use_clmul)
+ cipher.update(tv.aad)
+ if "FAIL" in tv.others:
+ self.assertRaises(ValueError, cipher.decrypt_and_verify,
+ tv.ct, tv.tag)
+ else:
+ pt = cipher.decrypt_and_verify(tv.ct, tv.tag)
+ self.assertEqual(pt, tv.pt)
+
+ setattr(NISTTestVectorsGCM, "test_%d" % idx, single_test)
+ setattr(NISTTestVectorsGCM_no_clmul, "test_%d" % idx, single_test)
+
+
+class TestVectorsWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings, **extra_params):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._extra_params = extra_params
+ self._id = "None"
+
+ def setUp(self):
+
+ def filter_tag(group):
+ return group['tagSize'] // 8
+
+ self.tv = load_test_vectors_wycheproof(("Cipher", "wycheproof"),
+ "aes_gcm_test.json",
+ "Wycheproof GCM",
+ group_tag={'tag_size': filter_tag})
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_encrypt(self, tv):
+ self._id = "Wycheproof Encrypt GCM Test #" + str(tv.id)
+
+ try:
+ cipher = AES.new(tv.key, AES.MODE_GCM, tv.iv, mac_len=tv.tag_size,
+ **self._extra_params)
+ except ValueError as e:
+ if len(tv.iv) == 0 and "Nonce cannot be empty" in str(e):
+ return
+ raise e
+
+ cipher.update(tv.aad)
+ ct, tag = cipher.encrypt_and_digest(tv.msg)
+ if tv.valid:
+ self.assertEqual(ct, tv.ct)
+ self.assertEqual(tag, tv.tag)
+ self.warn(tv)
+
+ def test_decrypt(self, tv):
+ self._id = "Wycheproof Decrypt GCM Test #" + str(tv.id)
+
+ try:
+ cipher = AES.new(tv.key, AES.MODE_GCM, tv.iv, mac_len=tv.tag_size,
+ **self._extra_params)
+ except ValueError as e:
+ if len(tv.iv) == 0 and "Nonce cannot be empty" in str(e):
+ return
+ raise e
+
+ cipher.update(tv.aad)
+ try:
+ pt = cipher.decrypt_and_verify(tv.ct, tv.tag)
+ except ValueError:
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.assertEqual(pt, tv.msg)
+ self.warn(tv)
+
+ def test_corrupt_decrypt(self, tv):
+ self._id = "Wycheproof Corrupt Decrypt GCM Test #" + str(tv.id)
+ if len(tv.iv) == 0 or len(tv.ct) < 1:
+ return
+ cipher = AES.new(tv.key, AES.MODE_GCM, tv.iv, mac_len=tv.tag_size,
+ **self._extra_params)
+ cipher.update(tv.aad)
+ ct_corrupt = strxor(tv.ct, b"\x00" * (len(tv.ct) - 1) + b"\x01")
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct_corrupt, tv.tag)
+
+ def runTest(self):
+
+ for tv in self.tv:
+ self.test_encrypt(tv)
+ self.test_decrypt(tv)
+ self.test_corrupt_decrypt(tv)
+
+
+class TestVariableLength(unittest.TestCase):
+
+ def __init__(self, **extra_params):
+ unittest.TestCase.__init__(self)
+ self._extra_params = extra_params
+
+ def runTest(self):
+ key = b'0' * 16
+ h = SHA256.new()
+
+ for length in range(160):
+ nonce = '{0:04d}'.format(length).encode('utf-8')
+ data = bchr(length) * length
+ cipher = AES.new(key, AES.MODE_GCM, nonce=nonce, **self._extra_params)
+ ct, tag = cipher.encrypt_and_digest(data)
+ h.update(ct)
+ h.update(tag)
+
+ self.assertEqual(h.hexdigest(), "7b7eb1ffbe67a2e53a912067c0ec8e62ebc7ce4d83490ea7426941349811bdf4")
+
+
+def get_tests(config={}):
+ from Crypto.Util import _cpu_features
+
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ tests = []
+ tests += list_test_cases(GcmTests)
+ tests += list_test_cases(GcmFSMTests)
+ tests += [TestVectors()]
+ tests += [TestVectorsWycheproof(wycheproof_warnings)]
+ tests += list_test_cases(TestVectorsGueronKrasnov)
+ tests += [TestVariableLength()]
+ if config.get('slow_tests'):
+ tests += list_test_cases(NISTTestVectorsGCM)
+
+ if _cpu_features.have_clmul():
+ tests += [TestVectorsWycheproof(wycheproof_warnings, use_clmul=False)]
+ tests += [TestVariableLength(use_clmul=False)]
+ if config.get('slow_tests'):
+ tests += list_test_cases(NISTTestVectorsGCM_no_clmul)
+ else:
+ print("Skipping test of PCLMULDQD in AES GCM")
+
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_KW.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_KW.py
new file mode 100644
index 00000000..f43ca2eb
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_KW.py
@@ -0,0 +1,175 @@
+import unittest
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors_wycheproof
+
+from Crypto.Cipher import AES
+
+
+class KW_Tests(unittest.TestCase):
+
+ # From RFC3394
+ tvs = [
+ ("000102030405060708090A0B0C0D0E0F",
+ "00112233445566778899AABBCCDDEEFF",
+ "1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5"),
+ ("000102030405060708090A0B0C0D0E0F1011121314151617",
+ "00112233445566778899AABBCCDDEEFF",
+ "96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D"),
+ ("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
+ "00112233445566778899AABBCCDDEEFF",
+ "64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7"),
+ ("000102030405060708090A0B0C0D0E0F1011121314151617",
+ "00112233445566778899AABBCCDDEEFF0001020304050607",
+ "031D33264E15D33268F24EC260743EDCE1C6C7DDEE725A936BA814915C6762D2"),
+ ("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
+ "00112233445566778899AABBCCDDEEFF0001020304050607",
+ "A8F9BC1612C68B3FF6E6F4FBE30E71E4769C8B80A32CB8958CD5D17D6B254DA1"),
+ ("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F",
+ "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F",
+ "28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD21"),
+ ]
+
+ def test_rfc3394(self):
+ for tv in self.tvs:
+ kek, pt, ct = [bytes.fromhex(x) for x in tv]
+
+ cipher = AES.new(kek, AES.MODE_KW)
+ ct2 = cipher.seal(pt)
+
+ self.assertEqual(ct, ct2)
+
+ cipher = AES.new(kek, AES.MODE_KW)
+ pt2 = cipher.unseal(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_neg1(self):
+
+ cipher = AES.new(b'-' * 16, AES.MODE_KW)
+
+ with self.assertRaises(ValueError):
+ cipher.seal(b'')
+
+ with self.assertRaises(ValueError):
+ cipher.seal(b'8' * 17)
+
+ def test_neg2(self):
+
+ cipher = AES.new(b'-' * 16, AES.MODE_KW)
+ ct = bytearray(cipher.seal(b'7' * 16))
+
+ cipher = AES.new(b'-' * 16, AES.MODE_KW)
+ cipher.unseal(ct)
+
+ cipher = AES.new(b'-' * 16, AES.MODE_KW)
+ ct[0] ^= 0xFF
+ with self.assertRaises(ValueError):
+ cipher.unseal(ct)
+
+
+class KW_Wycheproof(unittest.TestCase):
+
+ def setUp(self):
+ self.vectors = load_test_vectors_wycheproof(("Cipher", "wycheproof"),
+ "kw_test.json",
+ "Wycheproof tests for KW")
+
+ def test_wycheproof(self):
+
+ if not self.vectors:
+ self.skipTest("No test vectors available")
+
+ for vector in self.vectors:
+ with self.subTest(testId=vector.id):
+ cipher = AES.new(vector.key, AES.MODE_KW)
+
+ try:
+ cipher.seal(vector.msg)
+ except ValueError:
+ if vector.valid:
+ raise
+ continue
+
+ cipher = AES.new(vector.key, AES.MODE_KW)
+ try:
+ pt = cipher.unseal(vector.ct)
+ except ValueError:
+ if vector.valid:
+ raise
+ continue
+
+ self.assertEqual(pt, vector.msg)
+
+
+class KWP_Tests(unittest.TestCase):
+
+ tvs = [
+ ("5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8",
+ "c37b7e6492584340bed12207808941155068f738",
+ "138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a"),
+ ("5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8",
+ "466f7250617369",
+ "afbeb0f07dfbf5419200f2ccb50bb24f"),
+ ]
+
+ def test_rfc5649(self):
+ for tv in self.tvs:
+ kek, pt, ct = [bytes.fromhex(x) for x in tv]
+
+ cipher = AES.new(kek, AES.MODE_KWP)
+ ct2 = cipher.seal(pt)
+
+ self.assertEqual(ct, ct2)
+
+ cipher = AES.new(kek, AES.MODE_KWP)
+ pt2 = cipher.unseal(ct)
+ self.assertEqual(pt, pt2)
+
+
+class KWP_Wycheproof(unittest.TestCase):
+
+ def setUp(self):
+ self.vectors = load_test_vectors_wycheproof(("Cipher", "wycheproof"),
+ "kwp_test.json",
+ "Wycheproof tests for KWP")
+
+ def test_wycheproof(self):
+
+ if not self.vectors:
+ self.skipTest("No test vectors available")
+
+ for vector in self.vectors:
+ with self.subTest(testId=vector.id):
+ cipher = AES.new(vector.key, AES.MODE_KWP)
+
+ try:
+ cipher.seal(vector.msg)
+ except ValueError:
+ if vector.valid and not vector.warning:
+ raise
+ continue
+
+ cipher = AES.new(vector.key, AES.MODE_KWP)
+ try:
+ pt = cipher.unseal(vector.ct)
+ except ValueError:
+ if vector.valid and not vector.warning:
+ raise
+ continue
+
+ self.assertEqual(pt, vector.msg)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(KW_Tests)
+ tests += list_test_cases(KWP_Tests)
+ tests += list_test_cases(KW_Wycheproof)
+ tests += list_test_cases(KWP_Wycheproof)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_OCB.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_OCB.py
new file mode 100644
index 00000000..6e1a47ca
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_OCB.py
@@ -0,0 +1,845 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.Util.py3compat import b, tobytes, bchr
+from Crypto.Util.number import long_to_bytes
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Cipher import AES
+from Crypto.Hash import SHAKE128
+
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+
+class OcbTests(unittest.TestCase):
+
+ key_128 = get_tag_random("key_128", 16)
+ nonce_96 = get_tag_random("nonce_128", 12)
+ data = get_tag_random("data", 128)
+
+ def test_loopback_128(self):
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ pt = get_tag_random("plaintext", 16 * 100)
+ ct, mac = cipher.encrypt_and_digest(pt)
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ pt2 = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(pt, pt2)
+
+ def test_nonce(self):
+ # Nonce is optional
+ AES.new(self.key_128, AES.MODE_OCB)
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, self.nonce_96)
+ ct = cipher.encrypt(self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ self.assertEqual(ct, cipher.encrypt(self.data))
+
+ def test_nonce_must_be_bytes(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_OCB,
+ nonce=u'test12345678')
+
+ def test_nonce_length(self):
+ # nonce cannot be empty
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_OCB,
+ nonce=b(""))
+
+ # nonce can be up to 15 bytes long
+ for length in range(1, 16):
+ AES.new(self.key_128, AES.MODE_OCB, nonce=self.data[:length])
+
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_OCB,
+ nonce=self.data)
+
+ def test_block_size_128(self):
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ self.assertEqual(cipher.block_size, AES.block_size)
+
+ # By default, a 15 bytes long nonce is randomly generated
+ nonce1 = AES.new(self.key_128, AES.MODE_OCB).nonce
+ nonce2 = AES.new(self.key_128, AES.MODE_OCB).nonce
+ self.assertEqual(len(nonce1), 15)
+ self.assertNotEqual(nonce1, nonce2)
+
+ def test_nonce_attribute(self):
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ self.assertEqual(cipher.nonce, self.nonce_96)
+
+ # By default, a 15 bytes long nonce is randomly generated
+ nonce1 = AES.new(self.key_128, AES.MODE_OCB).nonce
+ nonce2 = AES.new(self.key_128, AES.MODE_OCB).nonce
+ self.assertEqual(len(nonce1), 15)
+ self.assertNotEqual(nonce1, nonce2)
+
+ def test_unknown_parameters(self):
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_OCB,
+ self.nonce_96, 7)
+ self.assertRaises(TypeError, AES.new, self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96, unknown=7)
+
+ # But some are only known by the base cipher
+ # (e.g. use_aesni consumed by the AES module)
+ AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96,
+ use_aesni=False)
+
+ def test_null_encryption_decryption(self):
+ for func in "encrypt", "decrypt":
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ result = getattr(cipher, func)(b(""))
+ self.assertEqual(result, b(""))
+
+ def test_either_encrypt_or_decrypt(self):
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.encrypt(b("xyz"))
+ self.assertRaises(TypeError, cipher.decrypt, b("xyz"))
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.decrypt(b("xyz"))
+ self.assertRaises(TypeError, cipher.encrypt, b("xyz"))
+
+ def test_data_must_be_bytes(self):
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt, u'test1234567890-*')
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt, u'test1234567890-*')
+
+ def test_mac_len(self):
+ # Invalid MAC length
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96, mac_len=7)
+ self.assertRaises(ValueError, AES.new, self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96, mac_len=16+1)
+
+ # Valid MAC length
+ for mac_len in range(8, 16 + 1):
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96,
+ mac_len=mac_len)
+ _, mac = cipher.encrypt_and_digest(self.data)
+ self.assertEqual(len(mac), mac_len)
+
+ # Default MAC length
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ _, mac = cipher.encrypt_and_digest(self.data)
+ self.assertEqual(len(mac), 16)
+
+ def test_invalid_mac(self):
+ from Crypto.Util.strxor import strxor_c
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ ct, mac = cipher.encrypt_and_digest(self.data)
+
+ invalid_mac = strxor_c(mac, 0x01)
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct,
+ invalid_mac)
+
+ def test_hex_mac(self):
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ mac_hex = cipher.hexdigest()
+ self.assertEqual(cipher.digest(), unhexlify(mac_hex))
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.hexverify(mac_hex)
+
+ def test_message_chunks(self):
+ # Validate that both associated data and plaintext/ciphertext
+ # can be broken up in chunks of arbitrary length
+
+ auth_data = get_tag_random("authenticated data", 127)
+ plaintext = get_tag_random("plaintext", 127)
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.update(auth_data)
+ ciphertext, ref_mac = cipher.encrypt_and_digest(plaintext)
+
+ def break_up(data, chunk_length):
+ return [data[i:i+chunk_length] for i in range(0, len(data),
+ chunk_length)]
+
+ # Encryption
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+
+ for chunk in break_up(auth_data, chunk_length):
+ cipher.update(chunk)
+ pt2 = b("")
+ for chunk in break_up(ciphertext, chunk_length):
+ pt2 += cipher.decrypt(chunk)
+ pt2 += cipher.decrypt()
+ self.assertEqual(plaintext, pt2)
+ cipher.verify(ref_mac)
+
+ # Decryption
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+
+ for chunk in break_up(auth_data, chunk_length):
+ cipher.update(chunk)
+ ct2 = b("")
+ for chunk in break_up(plaintext, chunk_length):
+ ct2 += cipher.encrypt(chunk)
+ ct2 += cipher.encrypt()
+ self.assertEqual(ciphertext, ct2)
+ self.assertEqual(cipher.digest(), ref_mac)
+
+ def test_bytearray(self):
+
+ # Encrypt
+ key_ba = bytearray(self.key_128)
+ nonce_ba = bytearray(self.nonce_96)
+ header_ba = bytearray(self.data)
+ data_ba = bytearray(self.data)
+
+ cipher1 = AES.new(self.key_128,
+ AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher1.update(self.data)
+ ct = cipher1.encrypt(self.data) + cipher1.encrypt()
+ tag = cipher1.digest()
+
+ cipher2 = AES.new(key_ba,
+ AES.MODE_OCB,
+ nonce=nonce_ba)
+ key_ba[:3] = b"\xFF\xFF\xFF"
+ nonce_ba[:3] = b"\xFF\xFF\xFF"
+ cipher2.update(header_ba)
+ header_ba[:3] = b"\xFF\xFF\xFF"
+ ct_test = cipher2.encrypt(data_ba) + cipher2.encrypt()
+ data_ba[:3] = b"\xFF\xFF\xFF"
+ tag_test = cipher2.digest()
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key_ba = bytearray(self.key_128)
+ nonce_ba = bytearray(self.nonce_96)
+ header_ba = bytearray(self.data)
+ del data_ba
+
+ cipher4 = AES.new(key_ba,
+ AES.MODE_OCB,
+ nonce=nonce_ba)
+ key_ba[:3] = b"\xFF\xFF\xFF"
+ nonce_ba[:3] = b"\xFF\xFF\xFF"
+ cipher4.update(header_ba)
+ header_ba[:3] = b"\xFF\xFF\xFF"
+ pt_test = cipher4.decrypt_and_verify(bytearray(ct_test), bytearray(tag_test))
+
+ self.assertEqual(self.data, pt_test)
+
+ def test_memoryview(self):
+
+ # Encrypt
+ key_mv = memoryview(bytearray(self.key_128))
+ nonce_mv = memoryview(bytearray(self.nonce_96))
+ header_mv = memoryview(bytearray(self.data))
+ data_mv = memoryview(bytearray(self.data))
+
+ cipher1 = AES.new(self.key_128,
+ AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher1.update(self.data)
+ ct = cipher1.encrypt(self.data) + cipher1.encrypt()
+ tag = cipher1.digest()
+
+ cipher2 = AES.new(key_mv,
+ AES.MODE_OCB,
+ nonce=nonce_mv)
+ key_mv[:3] = b"\xFF\xFF\xFF"
+ nonce_mv[:3] = b"\xFF\xFF\xFF"
+ cipher2.update(header_mv)
+ header_mv[:3] = b"\xFF\xFF\xFF"
+ ct_test = cipher2.encrypt(data_mv) + cipher2.encrypt()
+ data_mv[:3] = b"\xFF\xFF\xFF"
+ tag_test = cipher2.digest()
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key_mv = memoryview(bytearray(self.key_128))
+ nonce_mv = memoryview(bytearray(self.nonce_96))
+ header_mv = memoryview(bytearray(self.data))
+ del data_mv
+
+ cipher4 = AES.new(key_mv,
+ AES.MODE_OCB,
+ nonce=nonce_mv)
+ key_mv[:3] = b"\xFF\xFF\xFF"
+ nonce_mv[:3] = b"\xFF\xFF\xFF"
+ cipher4.update(header_mv)
+ header_mv[:3] = b"\xFF\xFF\xFF"
+ pt_test = cipher4.decrypt_and_verify(memoryview(ct_test), memoryview(tag_test))
+
+ self.assertEqual(self.data, pt_test)
+
+
+class OcbFSMTests(unittest.TestCase):
+
+ key_128 = get_tag_random("key_128", 16)
+ nonce_96 = get_tag_random("nonce_128", 12)
+ data = get_tag_random("data", 128)
+
+ def test_valid_init_encrypt_decrypt_digest_verify(self):
+ # No authenticated data, fixed plaintext
+ # Verify path INIT->ENCRYPT->ENCRYPT(NONE)->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data)
+ ct += cipher.encrypt()
+ mac = cipher.digest()
+
+ # Verify path INIT->DECRYPT->DECRYPT(NONCE)->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ cipher.decrypt()
+ cipher.verify(mac)
+
+ def test_invalid_init_encrypt_decrypt_digest_verify(self):
+ # No authenticated data, fixed plaintext
+ # Verify path INIT->ENCRYPT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data)
+ self.assertRaises(TypeError, cipher.digest)
+
+ # Verify path INIT->DECRYPT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ self.assertRaises(TypeError, cipher.verify)
+
+ def test_valid_init_update_digest_verify(self):
+ # No plaintext, fixed authenticated data
+ # Verify path INIT->UPDATE->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ cipher.verify(mac)
+
+ def test_valid_full_path(self):
+ # Fixed authenticated data, fixed plaintext
+ # Verify path INIT->UPDATE->ENCRYPT->ENCRYPT(NONE)->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ ct = cipher.encrypt(self.data)
+ ct += cipher.encrypt()
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->DECRYPT->DECRYPT(NONE)->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ cipher.decrypt(ct)
+ cipher.decrypt()
+ cipher.verify(mac)
+
+ # Verify path INIT->UPDATE->ENCRYPT->ENCRYPT_AND_DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ ct1 = cipher.encrypt(self.data[:2])
+ ct2, mac = cipher.encrypt_and_digest(self.data[2:])
+
+ # Verify path INIT->UPDATE->DECRYPT->DECRYPT_AND_VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ cipher.decrypt(ct1)
+ cipher.decrypt_and_verify(ct2, mac)
+
+ def test_invalid_encrypt_after_final(self):
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ cipher.encrypt(self.data)
+ cipher.encrypt()
+ self.assertRaises(TypeError, cipher.encrypt, self.data)
+
+ def test_invalid_decrypt_after_final(self):
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ cipher.decrypt(self.data)
+ cipher.decrypt()
+ self.assertRaises(TypeError, cipher.decrypt, self.data)
+
+ def test_valid_init_digest(self):
+ # Verify path INIT->DIGEST
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.digest()
+
+ def test_valid_init_verify(self):
+ # Verify path INIT->VERIFY
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ mac = cipher.digest()
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.verify(mac)
+
+ def test_valid_multiple_encrypt_or_decrypt(self):
+ for method_name in "encrypt", "decrypt":
+ for auth_data in (None, b("333"), self.data,
+ self.data + b("3")):
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ if auth_data is not None:
+ cipher.update(auth_data)
+ method = getattr(cipher, method_name)
+ method(self.data)
+ method(self.data)
+ method(self.data)
+ method(self.data)
+ method()
+
+ def test_valid_multiple_digest_or_verify(self):
+ # Multiple calls to digest
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.update(self.data)
+ first_mac = cipher.digest()
+ for x in range(4):
+ self.assertEqual(first_mac, cipher.digest())
+
+ # Multiple calls to verify
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.update(self.data)
+ for x in range(5):
+ cipher.verify(first_mac)
+
+ def test_valid_encrypt_and_digest_decrypt_and_verify(self):
+ # encrypt_and_digest
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.update(self.data)
+ ct, mac = cipher.encrypt_and_digest(self.data)
+
+ # decrypt_and_verify
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.update(self.data)
+ pt = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(self.data, pt)
+
+ def test_invalid_mixing_encrypt_decrypt(self):
+ # Once per method, with or without assoc. data
+ for method1_name, method2_name in (("encrypt", "decrypt"),
+ ("decrypt", "encrypt")):
+ for assoc_data_present in (True, False):
+ cipher = AES.new(self.key_128, AES.MODE_OCB,
+ nonce=self.nonce_96)
+ if assoc_data_present:
+ cipher.update(self.data)
+ getattr(cipher, method1_name)(self.data)
+ self.assertRaises(TypeError, getattr(cipher, method2_name),
+ self.data)
+
+ def test_invalid_encrypt_or_update_after_digest(self):
+ for method_name in "encrypt", "update":
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.encrypt(self.data)
+ cipher.encrypt()
+ cipher.digest()
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.encrypt_and_digest(self.data)
+
+ def test_invalid_decrypt_or_update_after_verify(self):
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ ct = cipher.encrypt(self.data)
+ ct += cipher.encrypt()
+ mac = cipher.digest()
+
+ for method_name in "decrypt", "update":
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.decrypt(ct)
+ cipher.decrypt()
+ cipher.verify(mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data)
+
+ cipher = AES.new(self.key_128, AES.MODE_OCB, nonce=self.nonce_96)
+ cipher.decrypt_and_verify(ct, mac)
+ self.assertRaises(TypeError, getattr(cipher, method_name),
+ self.data)
+
+
+def algo_rfc7253(keylen, taglen, noncelen):
+ """Implement the algorithm at page 18 of RFC 7253"""
+
+ key = bchr(0) * (keylen // 8 - 1) + bchr(taglen)
+ C = b""
+
+ for i in range(128):
+ S = bchr(0) * i
+
+ N = long_to_bytes(3 * i + 1, noncelen // 8)
+ cipher = AES.new(key, AES.MODE_OCB, nonce=N, mac_len=taglen // 8)
+ cipher.update(S)
+ C += cipher.encrypt(S) + cipher.encrypt() + cipher.digest()
+
+ N = long_to_bytes(3 * i + 2, noncelen // 8)
+ cipher = AES.new(key, AES.MODE_OCB, nonce=N, mac_len=taglen // 8)
+ C += cipher.encrypt(S) + cipher.encrypt() + cipher.digest()
+
+ N = long_to_bytes(3 * i + 3, noncelen // 8)
+ cipher = AES.new(key, AES.MODE_OCB, nonce=N, mac_len=taglen // 8)
+ cipher.update(S)
+ C += cipher.encrypt() + cipher.digest()
+
+ N = long_to_bytes(385, noncelen // 8)
+ cipher = AES.new(key, AES.MODE_OCB, nonce=N, mac_len=taglen // 8)
+ cipher.update(C)
+ return cipher.encrypt() + cipher.digest()
+
+
+class OcbRfc7253Test(unittest.TestCase):
+
+ # Tuple with
+ # - nonce
+ # - authenticated data
+ # - plaintext
+ # - ciphertext and 16 byte MAC tag
+ tv1_key = "000102030405060708090A0B0C0D0E0F"
+ tv1 = (
+ (
+ "BBAA99887766554433221100",
+ "",
+ "",
+ "785407BFFFC8AD9EDCC5520AC9111EE6"
+ ),
+ (
+ "BBAA99887766554433221101",
+ "0001020304050607",
+ "0001020304050607",
+ "6820B3657B6F615A5725BDA0D3B4EB3A257C9AF1F8F03009"
+ ),
+ (
+ "BBAA99887766554433221102",
+ "0001020304050607",
+ "",
+ "81017F8203F081277152FADE694A0A00"
+ ),
+ (
+ "BBAA99887766554433221103",
+ "",
+ "0001020304050607",
+ "45DD69F8F5AAE72414054CD1F35D82760B2CD00D2F99BFA9"
+ ),
+ (
+ "BBAA99887766554433221104",
+ "000102030405060708090A0B0C0D0E0F",
+ "000102030405060708090A0B0C0D0E0F",
+ "571D535B60B277188BE5147170A9A22C3AD7A4FF3835B8C5"
+ "701C1CCEC8FC3358"
+ ),
+ (
+ "BBAA99887766554433221105",
+ "000102030405060708090A0B0C0D0E0F",
+ "",
+ "8CF761B6902EF764462AD86498CA6B97"
+ ),
+ (
+ "BBAA99887766554433221106",
+ "",
+ "000102030405060708090A0B0C0D0E0F",
+ "5CE88EC2E0692706A915C00AEB8B2396F40E1C743F52436B"
+ "DF06D8FA1ECA343D"
+ ),
+ (
+ "BBAA99887766554433221107",
+ "000102030405060708090A0B0C0D0E0F1011121314151617",
+ "000102030405060708090A0B0C0D0E0F1011121314151617",
+ "1CA2207308C87C010756104D8840CE1952F09673A448A122"
+ "C92C62241051F57356D7F3C90BB0E07F"
+ ),
+ (
+ "BBAA99887766554433221108",
+ "000102030405060708090A0B0C0D0E0F1011121314151617",
+ "",
+ "6DC225A071FC1B9F7C69F93B0F1E10DE"
+ ),
+ (
+ "BBAA99887766554433221109",
+ "",
+ "000102030405060708090A0B0C0D0E0F1011121314151617",
+ "221BD0DE7FA6FE993ECCD769460A0AF2D6CDED0C395B1C3C"
+ "E725F32494B9F914D85C0B1EB38357FF"
+ ),
+ (
+ "BBAA9988776655443322110A",
+ "000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F",
+ "000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F",
+ "BD6F6C496201C69296C11EFD138A467ABD3C707924B964DE"
+ "AFFC40319AF5A48540FBBA186C5553C68AD9F592A79A4240"
+ ),
+ (
+ "BBAA9988776655443322110B",
+ "000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F",
+ "",
+ "FE80690BEE8A485D11F32965BC9D2A32"
+ ),
+ (
+ "BBAA9988776655443322110C",
+ "",
+ "000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F",
+ "2942BFC773BDA23CABC6ACFD9BFD5835BD300F0973792EF4"
+ "6040C53F1432BCDFB5E1DDE3BC18A5F840B52E653444D5DF"
+ ),
+ (
+ "BBAA9988776655443322110D",
+ "000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F2021222324252627",
+ "000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F2021222324252627",
+ "D5CA91748410C1751FF8A2F618255B68A0A12E093FF45460"
+ "6E59F9C1D0DDC54B65E8628E568BAD7AED07BA06A4A69483"
+ "A7035490C5769E60"
+ ),
+ (
+ "BBAA9988776655443322110E",
+ "000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F2021222324252627",
+ "",
+ "C5CD9D1850C141E358649994EE701B68"
+ ),
+ (
+ "BBAA9988776655443322110F",
+ "",
+ "000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F2021222324252627",
+ "4412923493C57D5DE0D700F753CCE0D1D2D95060122E9F15"
+ "A5DDBFC5787E50B5CC55EE507BCB084E479AD363AC366B95"
+ "A98CA5F3000B1479"
+ )
+ )
+
+ # Tuple with
+ # - key
+ # - nonce
+ # - authenticated data
+ # - plaintext
+ # - ciphertext and 12 byte MAC tag
+ tv2 = (
+ "0F0E0D0C0B0A09080706050403020100",
+ "BBAA9988776655443322110D",
+ "000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F2021222324252627",
+ "000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F2021222324252627",
+ "1792A4E31E0755FB03E31B22116E6C2DDF9EFD6E33D536F1"
+ "A0124B0A55BAE884ED93481529C76B6AD0C515F4D1CDD4FD"
+ "AC4F02AA"
+ )
+
+ # Tuple with
+ # - key length
+ # - MAC tag length
+ # - Expected output
+ tv3 = (
+ (128, 128, "67E944D23256C5E0B6C61FA22FDF1EA2"),
+ (192, 128, "F673F2C3E7174AAE7BAE986CA9F29E17"),
+ (256, 128, "D90EB8E9C977C88B79DD793D7FFA161C"),
+ (128, 96, "77A3D8E73589158D25D01209"),
+ (192, 96, "05D56EAD2752C86BE6932C5E"),
+ (256, 96, "5458359AC23B0CBA9E6330DD"),
+ (128, 64, "192C9B7BD90BA06A"),
+ (192, 64, "0066BC6E0EF34E24"),
+ (256, 64, "7D4EA5D445501CBE"),
+ )
+
+ def test1(self):
+ key = unhexlify(b(self.tv1_key))
+ for tv in self.tv1:
+ nonce, aad, pt, ct = [unhexlify(b(x)) for x in tv]
+ ct, mac_tag = ct[:-16], ct[-16:]
+
+ cipher = AES.new(key, AES.MODE_OCB, nonce=nonce)
+ cipher.update(aad)
+ ct2 = cipher.encrypt(pt) + cipher.encrypt()
+ self.assertEqual(ct, ct2)
+ self.assertEqual(mac_tag, cipher.digest())
+
+ cipher = AES.new(key, AES.MODE_OCB, nonce=nonce)
+ cipher.update(aad)
+ pt2 = cipher.decrypt(ct) + cipher.decrypt()
+ self.assertEqual(pt, pt2)
+ cipher.verify(mac_tag)
+
+ def test2(self):
+
+ key, nonce, aad, pt, ct = [unhexlify(b(x)) for x in self.tv2]
+ ct, mac_tag = ct[:-12], ct[-12:]
+
+ cipher = AES.new(key, AES.MODE_OCB, nonce=nonce, mac_len=12)
+ cipher.update(aad)
+ ct2 = cipher.encrypt(pt) + cipher.encrypt()
+ self.assertEqual(ct, ct2)
+ self.assertEqual(mac_tag, cipher.digest())
+
+ cipher = AES.new(key, AES.MODE_OCB, nonce=nonce, mac_len=12)
+ cipher.update(aad)
+ pt2 = cipher.decrypt(ct) + cipher.decrypt()
+ self.assertEqual(pt, pt2)
+ cipher.verify(mac_tag)
+
+ def test3(self):
+ for keylen, taglen, result in self.tv3:
+ result2 = algo_rfc7253(keylen, taglen, 96)
+ self.assertEqual(unhexlify(b(result)), result2)
+
+
+class OcbDkgTest(unittest.TestCase):
+ """Test vectors from https://gitlab.com/dkg/ocb-test-vectors"""
+
+ def test_1_2(self):
+ tvs = []
+ for fi in (1, 2):
+ for nb in (104, 112, 120):
+ tv_file = load_test_vectors(("Cipher", "AES"),
+ "test-vector-%d-nonce%d.txt" % (fi, nb),
+ "DKG tests, %d, %d bits" % (fi, nb),
+ {})
+ if tv_file is None:
+ break
+ key = tv_file[0].k
+ for tv in tv_file[1:]:
+ tv.k = key
+ tvs.append(tv)
+
+ for tv in tvs:
+ k, n, a, p, c = tv.k, tv.n, tv.a, tv.p, tv.c
+ mac_len = len(c) - len(p)
+ cipher = AES.new(k, AES.MODE_OCB, nonce=n, mac_len=mac_len)
+ cipher.update(a)
+ c_out, tag_out = cipher.encrypt_and_digest(p)
+ self.assertEqual(c, c_out + tag_out)
+
+ def test_3(self):
+
+ def check(keylen, taglen, noncelen, exp):
+ result = algo_rfc7253(keylen, taglen, noncelen)
+ self.assertEqual(result, unhexlify(exp))
+
+ # test-vector-3-nonce104.txt
+ check(128, 128, 104, "C47F5F0341E15326D4D1C46F47F05062")
+ check(192, 128, 104, "95B9167A38EB80495DFC561A8486E109")
+ check(256, 128, 104, "AFE1CDDB97028FD92F8FB3C8CFBA7D83")
+ check(128, 96, 104, "F471B4983BA80946DF217A54")
+ check(192, 96, 104, "5AE828BC51C24D85FA5CC7B2")
+ check(256, 96, 104, "8C8335982E2B734616CAD14C")
+ check(128, 64, 104, "B553F74B85FD1E5B")
+ check(192, 64, 104, "3B49D20E513531F9")
+ check(256, 64, 104, "ED6DA5B1216BF8BB")
+
+ # test-vector-3-nonce112.txt
+ check(128, 128, 112, "CA8AFCA031BAC3F480A583BD6C50A547")
+ check(192, 128, 112, "D170C1DF356308079DA9A3F619147148")
+ check(256, 128, 112, "57F94381F2F9231EFB04AECD323757C3")
+ check(128, 96, 112, "3A618B2531ED39F260C750DC")
+ check(192, 96, 112, "9071EB89FEDBADDA88FD286E")
+ check(256, 96, 112, "FDF0EFB97F21A39AC4BAB5AC")
+ check(128, 64, 112, "FAB2FF3A8DD82A13")
+ check(192, 64, 112, "AC01D912BD0737D3")
+ check(256, 64, 112, "9D1FD0B500EA4ECF")
+
+ # test-vector-3-nonce120.txt
+ check(128, 128, 120, "9E043A7140A25FB91F43BCC9DD7E0F46")
+ check(192, 128, 120, "680000E53908323A7F396B955B8EC641")
+ check(256, 128, 120, "8304B97FAACDA56E676602E1878A7E6F")
+ check(128, 96, 120, "81F978AC9867E825D339847D")
+ check(192, 96, 120, "EFCF2D60B24926ADA48CF5B1")
+ check(256, 96, 120, "84961DC56E917B165E58C174")
+ check(128, 64, 120, "227AEE6C9D905A61")
+ check(192, 64, 120, "541DE691B9E1A2F9")
+ check(256, 64, 120, "B0E761381C7129FC")
+
+ def test_2_bugfix(self):
+ nonce = unhexlify("EEDDCCBBAA9988776655443322110D")
+ key = unhexlify("0F0E0D0C0B0A09080706050403020100")
+ A = unhexlify("000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F2021222324252627")
+ P = unhexlify("000102030405060708090A0B0C0D0E0F1011121314151617"
+ "18191A1B1C1D1E1F2021222324252627")
+ C = unhexlify("07E903BFC49552411ABC865F5ECE60F6FAD1F5A9F14D3070"
+ "FA2F1308A563207FFE14C1EEA44B22059C7484319D8A2C53"
+ "C236A7B3")
+ mac_len = len(C) - len(P)
+
+ # Prior to version 3.17, a nonce of maximum length (15 bytes)
+ # was actually used as a 14 byte nonce. The last byte was erroneously
+ # ignored.
+ buggy_result = unhexlify("BA015C4E5AE54D76C890AE81BD40DC57"
+ "03EDC30E8AC2A58BC5D8FA4D61C5BAE6"
+ "C39BEAC435B2FD56A2A5085C1B135D77"
+ "0C8264B7")
+ cipher = AES.new(key, AES.MODE_OCB, nonce=nonce[:-1], mac_len=mac_len)
+ cipher.update(A)
+ C_out2, tag_out2 = cipher.encrypt_and_digest(P)
+ self.assertEqual(buggy_result, C_out2 + tag_out2)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(OcbTests)
+ tests += list_test_cases(OcbFSMTests)
+ tests += list_test_cases(OcbRfc7253Test)
+ tests += list_test_cases(OcbDkgTest)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_OFB.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_OFB.py
new file mode 100644
index 00000000..ec145ada
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_OFB.py
@@ -0,0 +1,238 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Util.py3compat import tobytes
+from Crypto.Cipher import AES, DES3, DES
+from Crypto.Hash import SHAKE128
+from Crypto.SelfTest.loader import load_test_vectors_wycheproof
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+from Crypto.SelfTest.Cipher.test_CBC import BlockChainingTests
+
+class OfbTests(BlockChainingTests):
+
+ aes_mode = AES.MODE_OFB
+ des3_mode = DES3.MODE_OFB
+
+ # Redefine test_unaligned_data_128/64
+
+ def test_unaligned_data_128(self):
+ plaintexts = [ b"7777777" ] * 100
+
+ cipher = AES.new(self.key_128, AES.MODE_CFB, self.iv_128, segment_size=8)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = AES.new(self.key_128, AES.MODE_CFB, self.iv_128, segment_size=8)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ cipher = AES.new(self.key_128, AES.MODE_CFB, self.iv_128, segment_size=128)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = AES.new(self.key_128, AES.MODE_CFB, self.iv_128, segment_size=128)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ def test_unaligned_data_64(self):
+ plaintexts = [ b"7777777" ] * 100
+ cipher = DES3.new(self.key_192, DES3.MODE_CFB, self.iv_64, segment_size=8)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = DES3.new(self.key_192, DES3.MODE_CFB, self.iv_64, segment_size=8)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ cipher = DES3.new(self.key_192, DES3.MODE_CFB, self.iv_64, segment_size=64)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = DES3.new(self.key_192, DES3.MODE_CFB, self.iv_64, segment_size=64)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+
+from Crypto.SelfTest.Cipher.test_CBC import NistBlockChainingVectors
+
+class NistOfbVectors(NistBlockChainingVectors):
+ aes_mode = AES.MODE_OFB
+ des_mode = DES.MODE_OFB
+ des3_mode = DES3.MODE_OFB
+
+
+# Create one test method per file
+nist_aes_kat_mmt_files = (
+ # KAT
+ "OFBGFSbox128.rsp",
+ "OFBGFSbox192.rsp",
+ "OFBGFSbox256.rsp",
+ "OFBKeySbox128.rsp",
+ "OFBKeySbox192.rsp",
+ "OFBKeySbox256.rsp",
+ "OFBVarKey128.rsp",
+ "OFBVarKey192.rsp",
+ "OFBVarKey256.rsp",
+ "OFBVarTxt128.rsp",
+ "OFBVarTxt192.rsp",
+ "OFBVarTxt256.rsp",
+ # MMT
+ "OFBMMT128.rsp",
+ "OFBMMT192.rsp",
+ "OFBMMT256.rsp",
+ )
+nist_aes_mct_files = (
+ "OFBMCT128.rsp",
+ "OFBMCT192.rsp",
+ "OFBMCT256.rsp",
+ )
+
+for file_name in nist_aes_kat_mmt_files:
+ def new_func(self, file_name=file_name):
+ self._do_kat_aes_test(file_name)
+ setattr(NistOfbVectors, "test_AES_" + file_name, new_func)
+
+for file_name in nist_aes_mct_files:
+ def new_func(self, file_name=file_name):
+ self._do_mct_aes_test(file_name)
+ setattr(NistOfbVectors, "test_AES_" + file_name, new_func)
+del file_name, new_func
+
+nist_tdes_files = (
+ "TOFBMMT2.rsp", # 2TDES
+ "TOFBMMT3.rsp", # 3TDES
+ "TOFBinvperm.rsp", # Single DES
+ "TOFBpermop.rsp",
+ "TOFBsubtab.rsp",
+ "TOFBvarkey.rsp",
+ "TOFBvartext.rsp",
+ )
+
+for file_name in nist_tdes_files:
+ def new_func(self, file_name=file_name):
+ self._do_tdes_test(file_name)
+ setattr(NistOfbVectors, "test_TDES_" + file_name, new_func)
+
+# END OF NIST OFB TEST VECTORS
+
+
+class SP800TestVectors(unittest.TestCase):
+ """Class exercising the OFB test vectors found in Section F.4
+ of NIST SP 800-3A"""
+
+ def test_aes_128(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = '3b3fd92eb72dad20333449f8e83cfb4a' +\
+ '7789508d16918f03f53c52dac54ed825' +\
+ '9740051e9c5fecf64344f7a82260edcc' +\
+ '304c6528f659c77866a510d9c1d6ae5e'
+ key = '2b7e151628aed2a6abf7158809cf4f3c'
+ iv = '000102030405060708090a0b0c0d0e0f'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.encrypt(plaintext[:-8]), ciphertext[:-8])
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.decrypt(ciphertext[:-8]), plaintext[:-8])
+
+ def test_aes_192(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = 'cdc80d6fddf18cab34c25909c99a4174' +\
+ 'fcc28b8d4c63837c09e81700c1100401' +\
+ '8d9a9aeac0f6596f559c6d4daf59a5f2' +\
+ '6d9f200857ca6c3e9cac524bd9acc92a'
+ key = '8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b'
+ iv = '000102030405060708090a0b0c0d0e0f'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.encrypt(plaintext[:-8]), ciphertext[:-8])
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.decrypt(ciphertext[:-8]), plaintext[:-8])
+
+ def test_aes_256(self):
+ plaintext = '6bc1bee22e409f96e93d7e117393172a' +\
+ 'ae2d8a571e03ac9c9eb76fac45af8e51' +\
+ '30c81c46a35ce411e5fbc1191a0a52ef' +\
+ 'f69f2445df4f9b17ad2b417be66c3710'
+ ciphertext = 'dc7e84bfda79164b7ecd8486985d3860' +\
+ '4febdc6740d20b3ac88f6ad82a4fb08d' +\
+ '71ab47a086e86eedf39d1c5bba97c408' +\
+ '0126141d67f37be8538f5a8be740e484'
+ key = '603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4'
+ iv = '000102030405060708090a0b0c0d0e0f'
+
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.encrypt(plaintext), ciphertext)
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.decrypt(ciphertext), plaintext)
+
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.encrypt(plaintext[:-8]), ciphertext[:-8])
+ cipher = AES.new(key, AES.MODE_OFB, iv)
+ self.assertEqual(cipher.decrypt(ciphertext[:-8]), plaintext[:-8])
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(OfbTests)
+ if config.get('slow_tests'):
+ tests += list_test_cases(NistOfbVectors)
+ tests += list_test_cases(SP800TestVectors)
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_OpenPGP.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_OpenPGP.py
new file mode 100644
index 00000000..e6cae670
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_OpenPGP.py
@@ -0,0 +1,218 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Util.py3compat import tobytes
+from Crypto.Cipher import AES, DES3, DES
+from Crypto.Hash import SHAKE128
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+
+from Crypto.SelfTest.Cipher.test_CBC import BlockChainingTests
+
+class OpenPGPTests(BlockChainingTests):
+
+ aes_mode = AES.MODE_OPENPGP
+ des3_mode = DES3.MODE_OPENPGP
+
+ # Redefine test_unaligned_data_128/64
+
+ key_128 = get_tag_random("key_128", 16)
+ key_192 = get_tag_random("key_192", 24)
+ iv_128 = get_tag_random("iv_128", 16)
+ iv_64 = get_tag_random("iv_64", 8)
+ data_128 = get_tag_random("data_128", 16)
+
+ def test_loopback_128(self):
+ cipher = AES.new(self.key_128, AES.MODE_OPENPGP, self.iv_128)
+ pt = get_tag_random("plaintext", 16 * 100)
+ ct = cipher.encrypt(pt)
+
+ eiv, ct = ct[:18], ct[18:]
+
+ cipher = AES.new(self.key_128, AES.MODE_OPENPGP, eiv)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_loopback_64(self):
+ cipher = DES3.new(self.key_192, DES3.MODE_OPENPGP, self.iv_64)
+ pt = get_tag_random("plaintext", 8 * 100)
+ ct = cipher.encrypt(pt)
+
+ eiv, ct = ct[:10], ct[10:]
+
+ cipher = DES3.new(self.key_192, DES3.MODE_OPENPGP, eiv)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def test_IV_iv_attributes(self):
+ cipher = AES.new(self.key_128, AES.MODE_OPENPGP, self.iv_128)
+ eiv = cipher.encrypt(b"")
+ self.assertEqual(cipher.iv, self.iv_128)
+
+ cipher = AES.new(self.key_128, AES.MODE_OPENPGP, eiv)
+ self.assertEqual(cipher.iv, self.iv_128)
+
+ def test_null_encryption_decryption(self):
+ cipher = AES.new(self.key_128, AES.MODE_OPENPGP, self.iv_128)
+ eiv = cipher.encrypt(b"")
+
+ cipher = AES.new(self.key_128, AES.MODE_OPENPGP, eiv)
+ self.assertEqual(cipher.decrypt(b""), b"")
+
+ def test_either_encrypt_or_decrypt(self):
+ cipher = AES.new(self.key_128, AES.MODE_OPENPGP, self.iv_128)
+ eiv = cipher.encrypt(b"")
+ self.assertRaises(TypeError, cipher.decrypt, b"")
+
+ cipher = AES.new(self.key_128, AES.MODE_OPENPGP, eiv)
+ cipher.decrypt(b"")
+ self.assertRaises(TypeError, cipher.encrypt, b"")
+
+ def test_unaligned_data_128(self):
+ plaintexts = [ b"7777777" ] * 100
+
+ cipher = AES.new(self.key_128, AES.MODE_OPENPGP, self.iv_128)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = AES.new(self.key_128, AES.MODE_OPENPGP, self.iv_128)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ def test_unaligned_data_64(self):
+ plaintexts = [ b"7777777" ] * 100
+
+ cipher = DES3.new(self.key_192, DES3.MODE_OPENPGP, self.iv_64)
+ ciphertexts = [ cipher.encrypt(x) for x in plaintexts ]
+ cipher = DES3.new(self.key_192, DES3.MODE_OPENPGP, self.iv_64)
+ self.assertEqual(b"".join(ciphertexts), cipher.encrypt(b"".join(plaintexts)))
+
+ def test_output_param(self):
+ pass
+
+ def test_output_param_same_buffer(self):
+ pass
+
+ def test_output_param_memoryview(self):
+ pass
+
+ def test_output_param_neg(self):
+ pass
+
+
+class TestVectors(unittest.TestCase):
+
+ def test_aes(self):
+ # The following test vectors have been generated with gpg v1.4.0.
+ # The command line used was:
+ #
+ # gpg -c -z 0 --cipher-algo AES --passphrase secret_passphrase \
+ # --disable-mdc --s2k-mode 0 --output ct pt
+ #
+ # As result, the content of the file 'pt' is encrypted with a key derived
+ # from 'secret_passphrase' and written to file 'ct'.
+ # Test vectors must be extracted from 'ct', which is a collection of
+ # TLVs (see RFC4880 for all details):
+ # - the encrypted data (with the encrypted IV as prefix) is the payload
+ # of the TLV with tag 9 (Symmetrical Encrypted Data Packet).
+ # This is the ciphertext in the test vector.
+ # - inside the encrypted part, there is a further layer of TLVs. One must
+ # look for tag 11 (Literal Data Packet); in its payload, after a short
+ # but time dependent header, there is the content of file 'pt'.
+ # In the test vector, the plaintext is the complete set of TLVs that gets
+ # encrypted. It is not just the content of 'pt'.
+ # - the key is the leftmost 16 bytes of the SHA1 digest of the password.
+ # The test vector contains such shortened digest.
+ #
+ # Note that encryption uses a clear IV, and decryption an encrypted IV
+
+ plaintext = 'ac18620270744fb4f647426c61636b4361745768697465436174'
+ ciphertext = 'dc6b9e1f095de609765c59983db5956ae4f63aea7405389d2ebb'
+ key = '5baa61e4c9b93f3f0682250b6cf8331b'
+ iv = '3d7d3e62282add7eb203eeba5c800733'
+ encrypted_iv='fd934601ef49cb58b6d9aebca6056bdb96ef'
+
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ encrypted_iv = unhexlify(encrypted_iv)
+
+ cipher = AES.new(key, AES.MODE_OPENPGP, iv)
+ ct = cipher.encrypt(plaintext)
+ self.assertEqual(ct[:18], encrypted_iv)
+ self.assertEqual(ct[18:], ciphertext)
+
+ cipher = AES.new(key, AES.MODE_OPENPGP, encrypted_iv)
+ pt = cipher.decrypt(ciphertext)
+ self.assertEqual(pt, plaintext)
+
+ def test_des3(self):
+ # The following test vectors have been generated with gpg v1.4.0.
+ # The command line used was:
+ # gpg -c -z 0 --cipher-algo 3DES --passphrase secret_passphrase \
+ # --disable-mdc --s2k-mode 0 --output ct pt
+ # For an explanation, see test_AES.py .
+
+ plaintext = 'ac1762037074324fb53ba3596f73656d69746556616c6c6579'
+ ciphertext = '9979238528357b90e2e0be549cb0b2d5999b9a4a447e5c5c7d'
+ key = '7ade65b460f5ea9be35f9e14aa883a2048e3824aa616c0b2'
+ iv='cd47e2afb8b7e4b0'
+ encrypted_iv='6a7eef0b58050e8b904a'
+
+ plaintext = unhexlify(plaintext)
+ ciphertext = unhexlify(ciphertext)
+ key = unhexlify(key)
+ iv = unhexlify(iv)
+ encrypted_iv = unhexlify(encrypted_iv)
+
+ cipher = DES3.new(key, DES3.MODE_OPENPGP, iv)
+ ct = cipher.encrypt(plaintext)
+ self.assertEqual(ct[:10], encrypted_iv)
+ self.assertEqual(ct[10:], ciphertext)
+
+ cipher = DES3.new(key, DES3.MODE_OPENPGP, encrypted_iv)
+ pt = cipher.decrypt(ciphertext)
+ self.assertEqual(pt, plaintext)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(OpenPGPTests)
+ tests += list_test_cases(TestVectors)
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_SIV.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_SIV.py
new file mode 100644
index 00000000..a80ddc1e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_SIV.py
@@ -0,0 +1,552 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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 json
+import unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors_wycheproof
+
+from Crypto.Util.py3compat import tobytes, bchr
+from Crypto.Cipher import AES
+from Crypto.Hash import SHAKE128
+
+from Crypto.Util.strxor import strxor
+
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+
+class SivTests(unittest.TestCase):
+
+ key_256 = get_tag_random("key_256", 32)
+ key_384 = get_tag_random("key_384", 48)
+ key_512 = get_tag_random("key_512", 64)
+ nonce_96 = get_tag_random("nonce_128", 12)
+ data = get_tag_random("data", 128)
+
+ def test_loopback_128(self):
+ for key in self.key_256, self.key_384, self.key_512:
+ cipher = AES.new(key, AES.MODE_SIV, nonce=self.nonce_96)
+ pt = get_tag_random("plaintext", 16 * 100)
+ ct, mac = cipher.encrypt_and_digest(pt)
+
+ cipher = AES.new(key, AES.MODE_SIV, nonce=self.nonce_96)
+ pt2 = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(pt, pt2)
+
+ def test_nonce(self):
+ # Deterministic encryption
+ AES.new(self.key_256, AES.MODE_SIV)
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, self.nonce_96)
+ ct1, tag1 = cipher.encrypt_and_digest(self.data)
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ ct2, tag2 = cipher.encrypt_and_digest(self.data)
+ self.assertEqual(ct1 + tag1, ct2 + tag2)
+
+ def test_nonce_must_be_bytes(self):
+ self.assertRaises(TypeError, AES.new, self.key_256, AES.MODE_SIV,
+ nonce=u'test12345678')
+
+ def test_nonce_length(self):
+ # nonce can be of any length (but not empty)
+ self.assertRaises(ValueError, AES.new, self.key_256, AES.MODE_SIV,
+ nonce=b"")
+
+ for x in range(1, 128):
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=bchr(1) * x)
+ cipher.encrypt_and_digest(b'\x01')
+
+ def test_block_size_128(self):
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ self.assertEqual(cipher.block_size, AES.block_size)
+
+ def test_nonce_attribute(self):
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ self.assertEqual(cipher.nonce, self.nonce_96)
+
+ # By default, no nonce is randomly generated
+ self.assertFalse(hasattr(AES.new(self.key_256, AES.MODE_SIV), "nonce"))
+
+ def test_unknown_parameters(self):
+ self.assertRaises(TypeError, AES.new, self.key_256, AES.MODE_SIV,
+ self.nonce_96, 7)
+ self.assertRaises(TypeError, AES.new, self.key_256, AES.MODE_SIV,
+ nonce=self.nonce_96, unknown=7)
+
+ # But some are only known by the base cipher
+ # (e.g. use_aesni consumed by the AES module)
+ AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96,
+ use_aesni=False)
+
+ def test_encrypt_excludes_decrypt(self):
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.encrypt_and_digest(self.data)
+ self.assertRaises(TypeError, cipher.decrypt, self.data)
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.encrypt_and_digest(self.data)
+ self.assertRaises(TypeError, cipher.decrypt_and_verify,
+ self.data, self.data)
+
+ def test_data_must_be_bytes(self):
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt, u'test1234567890-*')
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt_and_verify,
+ u'test1234567890-*', b"xxxx")
+
+ def test_mac_len(self):
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ _, mac = cipher.encrypt_and_digest(self.data)
+ self.assertEqual(len(mac), 16)
+
+ def test_invalid_mac(self):
+ from Crypto.Util.strxor import strxor_c
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ ct, mac = cipher.encrypt_and_digest(self.data)
+
+ invalid_mac = strxor_c(mac, 0x01)
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct,
+ invalid_mac)
+
+ def test_hex_mac(self):
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ mac_hex = cipher.hexdigest()
+ self.assertEqual(cipher.digest(), unhexlify(mac_hex))
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.hexverify(mac_hex)
+
+ def test_bytearray(self):
+
+ # Encrypt
+ key = bytearray(self.key_256)
+ nonce = bytearray(self.nonce_96)
+ data = bytearray(self.data)
+ header = bytearray(self.data)
+
+ cipher1 = AES.new(self.key_256,
+ AES.MODE_SIV,
+ nonce=self.nonce_96)
+ cipher1.update(self.data)
+ ct, tag = cipher1.encrypt_and_digest(self.data)
+
+ cipher2 = AES.new(key,
+ AES.MODE_SIV,
+ nonce=nonce)
+ key[:3] = b'\xFF\xFF\xFF'
+ nonce[:3] = b'\xFF\xFF\xFF'
+ cipher2.update(header)
+ header[:3] = b'\xFF\xFF\xFF'
+ ct_test, tag_test = cipher2.encrypt_and_digest(data)
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key = bytearray(self.key_256)
+ nonce = bytearray(self.nonce_96)
+ header = bytearray(self.data)
+ ct_ba = bytearray(ct)
+ tag_ba = bytearray(tag)
+
+ cipher3 = AES.new(key,
+ AES.MODE_SIV,
+ nonce=nonce)
+ key[:3] = b'\xFF\xFF\xFF'
+ nonce[:3] = b'\xFF\xFF\xFF'
+ cipher3.update(header)
+ header[:3] = b'\xFF\xFF\xFF'
+ pt_test = cipher3.decrypt_and_verify(ct_ba, tag_ba)
+
+ self.assertEqual(self.data, pt_test)
+
+ def test_memoryview(self):
+
+ # Encrypt
+ key = memoryview(bytearray(self.key_256))
+ nonce = memoryview(bytearray(self.nonce_96))
+ data = memoryview(bytearray(self.data))
+ header = memoryview(bytearray(self.data))
+
+ cipher1 = AES.new(self.key_256,
+ AES.MODE_SIV,
+ nonce=self.nonce_96)
+ cipher1.update(self.data)
+ ct, tag = cipher1.encrypt_and_digest(self.data)
+
+ cipher2 = AES.new(key,
+ AES.MODE_SIV,
+ nonce=nonce)
+ key[:3] = b'\xFF\xFF\xFF'
+ nonce[:3] = b'\xFF\xFF\xFF'
+ cipher2.update(header)
+ header[:3] = b'\xFF\xFF\xFF'
+ ct_test, tag_test= cipher2.encrypt_and_digest(data)
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(tag, tag_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decrypt
+ key = memoryview(bytearray(self.key_256))
+ nonce = memoryview(bytearray(self.nonce_96))
+ header = memoryview(bytearray(self.data))
+ ct_ba = memoryview(bytearray(ct))
+ tag_ba = memoryview(bytearray(tag))
+
+ cipher3 = AES.new(key,
+ AES.MODE_SIV,
+ nonce=nonce)
+ key[:3] = b'\xFF\xFF\xFF'
+ nonce[:3] = b'\xFF\xFF\xFF'
+ cipher3.update(header)
+ header[:3] = b'\xFF\xFF\xFF'
+ pt_test = cipher3.decrypt_and_verify(ct_ba, tag_ba)
+
+ self.assertEqual(self.data, pt_test)
+
+ def test_output_param(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ ct, tag = cipher.encrypt_and_digest(pt)
+
+ output = bytearray(128)
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ res, tag_out = cipher.encrypt_and_digest(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+ self.assertEqual(tag, tag_out)
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ res = cipher.decrypt_and_verify(ct, tag, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ def test_output_param_memoryview(self):
+
+ pt = b'5' * 128
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ ct, tag = cipher.encrypt_and_digest(pt)
+
+ output = memoryview(bytearray(128))
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.encrypt_and_digest(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.decrypt_and_verify(ct, tag, output=output)
+ self.assertEqual(pt, output)
+
+ def test_output_param_neg(self):
+ LEN_PT = 128
+
+ pt = b'5' * LEN_PT
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ ct, tag = cipher.encrypt_and_digest(pt)
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt_and_digest, pt, output=b'0' * LEN_PT)
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt_and_verify, ct, tag, output=b'0' * LEN_PT)
+
+ shorter_output = bytearray(LEN_PT - 1)
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.encrypt_and_digest, pt, output=shorter_output)
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ self.assertRaises(ValueError, cipher.decrypt_and_verify, ct, tag, output=shorter_output)
+
+
+class SivFSMTests(unittest.TestCase):
+
+ key_256 = get_tag_random("key_256", 32)
+ nonce_96 = get_tag_random("nonce_96", 12)
+ data = get_tag_random("data", 128)
+
+ def test_invalid_init_encrypt(self):
+ # Path INIT->ENCRYPT fails
+ cipher = AES.new(self.key_256, AES.MODE_SIV,
+ nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.encrypt, b"xxx")
+
+ def test_invalid_init_decrypt(self):
+ # Path INIT->DECRYPT fails
+ cipher = AES.new(self.key_256, AES.MODE_SIV,
+ nonce=self.nonce_96)
+ self.assertRaises(TypeError, cipher.decrypt, b"xxx")
+
+ def test_valid_init_update_digest_verify(self):
+ # No plaintext, fixed authenticated data
+ # Verify path INIT->UPDATE->DIGEST
+ cipher = AES.new(self.key_256, AES.MODE_SIV,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ mac = cipher.digest()
+
+ # Verify path INIT->UPDATE->VERIFY
+ cipher = AES.new(self.key_256, AES.MODE_SIV,
+ nonce=self.nonce_96)
+ cipher.update(self.data)
+ cipher.verify(mac)
+
+ def test_valid_init_digest(self):
+ # Verify path INIT->DIGEST
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.digest()
+
+ def test_valid_init_verify(self):
+ # Verify path INIT->VERIFY
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ mac = cipher.digest()
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.verify(mac)
+
+ def test_valid_multiple_digest_or_verify(self):
+ # Multiple calls to digest
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.update(self.data)
+ first_mac = cipher.digest()
+ for x in range(4):
+ self.assertEqual(first_mac, cipher.digest())
+
+ # Multiple calls to verify
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.update(self.data)
+ for x in range(5):
+ cipher.verify(first_mac)
+
+ def test_valid_encrypt_and_digest_decrypt_and_verify(self):
+ # encrypt_and_digest
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.update(self.data)
+ ct, mac = cipher.encrypt_and_digest(self.data)
+
+ # decrypt_and_verify
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.update(self.data)
+ pt = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(self.data, pt)
+
+ def test_invalid_multiple_encrypt_and_digest(self):
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ ct, tag = cipher.encrypt_and_digest(self.data)
+ self.assertRaises(TypeError, cipher.encrypt_and_digest, b'')
+
+ def test_invalid_multiple_decrypt_and_verify(self):
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ ct, tag = cipher.encrypt_and_digest(self.data)
+
+ cipher = AES.new(self.key_256, AES.MODE_SIV, nonce=self.nonce_96)
+ cipher.decrypt_and_verify(ct, tag)
+ self.assertRaises(TypeError, cipher.decrypt_and_verify, ct, tag)
+
+
+def transform(tv):
+ new_tv = [[unhexlify(x) for x in tv[0].split("-")]]
+ new_tv += [ unhexlify(x) for x in tv[1:5]]
+ if tv[5]:
+ nonce = unhexlify(tv[5])
+ else:
+ nonce = None
+ new_tv += [ nonce ]
+ return new_tv
+
+
+class TestVectors(unittest.TestCase):
+ """Class exercising the SIV test vectors found in RFC5297"""
+
+ # This is a list of tuples with 5 items:
+ #
+ # 1. Header + '|' + plaintext
+ # 2. Header + '|' + ciphertext + '|' + MAC
+ # 3. AES-128 key
+ # 4. Description
+ # 5. Dictionary of parameters to be passed to AES.new().
+ # It must include the nonce.
+ #
+ # A "Header" is a dash ('-') separated sequece of components.
+ #
+ test_vectors_hex = [
+ (
+ '101112131415161718191a1b1c1d1e1f2021222324252627',
+ '112233445566778899aabbccddee',
+ '40c02b9690c4dc04daef7f6afe5c',
+ '85632d07c6e8f37f950acd320a2ecc93',
+ 'fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff',
+ None
+ ),
+ (
+ '00112233445566778899aabbccddeeffdeaddadadeaddadaffeeddccbbaa9988' +
+ '7766554433221100-102030405060708090a0',
+ '7468697320697320736f6d6520706c61696e7465787420746f20656e63727970' +
+ '74207573696e67205349562d414553',
+ 'cb900f2fddbe404326601965c889bf17dba77ceb094fa663b7a3f748ba8af829' +
+ 'ea64ad544a272e9c485b62a3fd5c0d',
+ '7bdb6e3b432667eb06f4d14bff2fbd0f',
+ '7f7e7d7c7b7a79787776757473727170404142434445464748494a4b4c4d4e4f',
+ '09f911029d74e35bd84156c5635688c0'
+ ),
+ ]
+
+ test_vectors = [ transform(tv) for tv in test_vectors_hex ]
+
+ def runTest(self):
+ for assoc_data, pt, ct, mac, key, nonce in self.test_vectors:
+
+ # Encrypt
+ cipher = AES.new(key, AES.MODE_SIV, nonce=nonce)
+ for x in assoc_data:
+ cipher.update(x)
+ ct2, mac2 = cipher.encrypt_and_digest(pt)
+ self.assertEqual(ct, ct2)
+ self.assertEqual(mac, mac2)
+
+ # Decrypt
+ cipher = AES.new(key, AES.MODE_SIV, nonce=nonce)
+ for x in assoc_data:
+ cipher.update(x)
+ pt2 = cipher.decrypt_and_verify(ct, mac)
+ self.assertEqual(pt, pt2)
+
+
+class TestVectorsWycheproof(unittest.TestCase):
+
+ def __init__(self):
+ unittest.TestCase.__init__(self)
+ self._id = "None"
+
+ def setUp(self):
+ self.tv = load_test_vectors_wycheproof(("Cipher", "wycheproof"),
+ "aes_siv_cmac_test.json",
+ "Wycheproof AES SIV")
+
+ def shortDescription(self):
+ return self._id
+
+ def test_encrypt(self, tv):
+ self._id = "Wycheproof Encrypt AES-SIV Test #" + str(tv.id)
+
+ cipher = AES.new(tv.key, AES.MODE_SIV)
+ cipher.update(tv.aad)
+ ct, tag = cipher.encrypt_and_digest(tv.msg)
+ if tv.valid:
+ self.assertEqual(tag + ct, tv.ct)
+
+ def test_decrypt(self, tv):
+ self._id = "Wycheproof Decrypt AES_SIV Test #" + str(tv.id)
+
+ cipher = AES.new(tv.key, AES.MODE_SIV)
+ cipher.update(tv.aad)
+ try:
+ pt = cipher.decrypt_and_verify(tv.ct[16:], tv.ct[:16])
+ except ValueError:
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.assertEqual(pt, tv.msg)
+
+ def runTest(self):
+
+ for tv in self.tv:
+ self.test_encrypt(tv)
+ self.test_decrypt(tv)
+
+
+class TestVectorsWycheproof2(unittest.TestCase):
+
+ def __init__(self):
+ unittest.TestCase.__init__(self)
+ self._id = "None"
+
+ def setUp(self):
+ self.tv = load_test_vectors_wycheproof(("Cipher", "wycheproof"),
+ "aead_aes_siv_cmac_test.json",
+ "Wycheproof AEAD SIV")
+
+ def shortDescription(self):
+ return self._id
+
+ def test_encrypt(self, tv):
+ self._id = "Wycheproof Encrypt AEAD-AES-SIV Test #" + str(tv.id)
+
+ cipher = AES.new(tv.key, AES.MODE_SIV, nonce=tv.iv)
+ cipher.update(tv.aad)
+ ct, tag = cipher.encrypt_and_digest(tv.msg)
+ if tv.valid:
+ self.assertEqual(ct, tv.ct)
+ self.assertEqual(tag, tv.tag)
+
+ def test_decrypt(self, tv):
+ self._id = "Wycheproof Decrypt AEAD-AES-SIV Test #" + str(tv.id)
+
+ cipher = AES.new(tv.key, AES.MODE_SIV, nonce=tv.iv)
+ cipher.update(tv.aad)
+ try:
+ pt = cipher.decrypt_and_verify(tv.ct, tv.tag)
+ except ValueError:
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.assertEqual(pt, tv.msg)
+
+ def runTest(self):
+
+ for tv in self.tv:
+ self.test_encrypt(tv)
+ self.test_decrypt(tv)
+
+
+def get_tests(config={}):
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ tests = []
+ tests += list_test_cases(SivTests)
+ tests += list_test_cases(SivFSMTests)
+ tests += [ TestVectors() ]
+ tests += [ TestVectorsWycheproof() ]
+ tests += [ TestVectorsWycheproof2() ]
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_Salsa20.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_Salsa20.py
new file mode 100644
index 00000000..a710462e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_Salsa20.py
@@ -0,0 +1,367 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/Salsa20.py: Self-test for the Salsa20 stream cipher
+#
+# Written in 2013 by Fabrizio Tarizzo
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Cipher.Salsa20"""
+
+import unittest
+
+from Crypto.Util.py3compat import bchr
+
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Cipher import Salsa20
+
+from .common import make_stream_tests
+
+# This is a list of (plaintext, ciphertext, key[, description[, params]])
+# tuples.
+test_data = [
+ # Test vectors are taken from
+ # http://www.ecrypt.eu.org/stream/svn/viewcvs.cgi/ecrypt/trunk/submissions/salsa20/full/verified.test-vectors
+ ( '00' * 512,
+ '4dfa5e481da23ea09a31022050859936da52fcee218005164f267cb65f5cfd7f'
+ + '2b4f97e0ff16924a52df269515110a07f9e460bc65ef95da58f740b7d1dbb0aa'
+ + 'd64cec189c7eb8c6bbf3d7376c80a481d43e628701f6a27afb9fe23919f24114'
+ + '8db44f70d7063efcc3dd55a0893a613c3c6fe1c127bd6f59910589293bb6ef9e'
+ + 'e24819066dee1a64f49b0bbad5988635272b169af861f85df881939f29ada6fd'
+ + '0241410e8d332ae4798d929434a2630de451ec4e0169694cbaa7ebb121ea6a2b'
+ + 'da9c1581f429e0a00f7d67e23b730676783b262e8eb43a25f55fb90b3e753aef'
+ + '8c6713ec66c51881111593ccb3e8cb8f8de124080501eeeb389c4bcb6977cf95'
+ + '7d5789631eb4554400e1e025935dfa7b3e9039d61bdc58a8697d36815bf1985c'
+ + 'efdf7ae112e5bb81e37ecf0616ce7147fc08a93a367e08631f23c03b00a8da2f'
+ + 'aa5024e5c8d30aca43fc2d5082067b21b234bc741d68fb292c6012c3764ccee3'
+ + '1e364a5403e00cfee338a21a01e7d3cefd5a770ca0ab48c435ea6116435f7ad8'
+ + '30b217b49f978a68e207ed9f462af7fb195b2115fe8f24f152e4ddc32202d6f2'
+ + 'b52fafbcfbc202d8a259a611e901d3f62d065eb13f09bbc45cd45119b843efaa'
+ + 'b375703739daced4dd4059fd71c3c47fc2f9939670fad4a46066adcc6a564578'
+ + '3308b90ffb72be04a6b147cbe38cc0c3b9267c296a92a7c69873f9f263be9703',
+ '80000000000000000000000000000000',
+ '128 bits key, set 1, vector 0',
+ dict (iv='00'*8)),
+
+ ( '00' * 512,
+ 'e3be8fdd8beca2e3ea8ef9475b29a6e7003951e1097a5c38d23b7a5fad9f6844'
+ + 'b22c97559e2723c7cbbd3fe4fc8d9a0744652a83e72a9c461876af4d7ef1a117'
+ + '8da2b74eef1b6283e7e20166abcae538e9716e4669e2816b6b20c5c356802001'
+ + 'cc1403a9a117d12a2669f456366d6ebb0f1246f1265150f793cdb4b253e348ae'
+ + '203d89bc025e802a7e0e00621d70aa36b7e07cb1e7d5b38d5e222b8b0e4b8407'
+ + '0142b1e29504767d76824850320b5368129fdd74e861b498e3be8d16f2d7d169'
+ + '57be81f47b17d9ae7c4ff15429a73e10acf250ed3a90a93c711308a74c6216a9'
+ + 'ed84cd126da7f28e8abf8bb63517e1ca98e712f4fb2e1a6aed9fdc73291faa17'
+ + '958211c4ba2ebd5838c635edb81f513a91a294e194f1c039aeec657dce40aa7e'
+ + '7c0af57cacefa40c9f14b71a4b3456a63e162ec7d8d10b8ffb1810d71001b618'
+ + '2f9f73da53b85405c11f7b2d890fa8ae0c7f2e926d8a98c7ec4e91b65120e988'
+ + '349631a700c6facec3471cb0413656e75e309456584084d7e12c5b43a41c43ed'
+ + '9a048abd9b880da65f6a665a20fe7b77cd292fe62cae644b7f7df69f32bdb331'
+ + '903e6505ce44fdc293920c6a9ec7057e23df7dad298f82ddf4efb7fdc7bfc622'
+ + '696afcfd0cddcc83c7e77f11a649d79acdc3354e9635ff137e929933a0bd6f53'
+ + '77efa105a3a4266b7c0d089d08f1e855cc32b15b93784a36e56a76cc64bc8477',
+ '8000000000000000000000000000000000000000000000000000000000000000',
+ '256 bits key, set 1, vector 0',
+ dict (iv='00'*8)),
+
+ ( '00' * 512,
+ '169060ccb42bea7bee4d8012a02f3635eb7bca12859fa159cd559094b3507db8'
+ + '01735d1a1300102a9c9415546829cbd2021ba217b39b81d89c55b13d0c603359'
+ + '3f84159a3c84f4b4f4a0edcd9d38ff261a737909e0b66d68b5cac496f3a5be99'
+ + 'cb12c321ab711afaab36cc0947955e1a9bb952ed54425e7711279fbc81bb83f5'
+ + '6e55cea44e6daddb05858a153ea6213b3350c12aa1a83ef2726f09485fa71790'
+ + 'f9b9f922c7dda1113b1f9d56658ed3402803f511bc1f122601d5e7f0ff036e23'
+ + '23ef24bb24195b9fd574823cd8a40c29d86bd35c191e2038779ff696c712b6d8'
+ + '2e7014dbe1ac5d527af076c088c4a8d44317958189f6ef54933a7e0816b5b916'
+ + 'd8f12ed8afe9422b85e5cc9b8adec9d6cfabe8dbc1082bccc02f5a7266aa074c'
+ + 'a284e583a35837798cc0e69d4ce937653b8cdd65ce414b89138615ccb165ad19'
+ + '3c6b9c3d05eef4be921a10ea811fe61d11c6867600188e065daff90b509ec56b'
+ + 'd41e7e8968c478c78d590c2d2ee24ea009c8f49bc3d81672cfc47895a9e21c9a'
+ + '471ebf8e294bee5d2de436ac8d052bf31111b345f1da23c3a4d13b9fc5f0900a'
+ + 'a298f98f538973b8fad40d4d159777de2cfe2a3dead1645ddb49794827dba040'
+ + 'f70a0ff4ecd155e0f033604693a51e2363880e2ecf98699e7174af7c2c6b0fc6'
+ + '59ae329599a3949272a37b9b2183a0910922a3f325ae124dcbdd735364055ceb',
+ '09090909090909090909090909090909',
+ '128 bits key, set 2, vector 9',
+ dict (iv='00'*8)),
+
+ ( '00' * 512,
+ '7041e747ceb22ed7812985465f50333124f971da1c5d6efe5ca201b886f31046'
+ + 'e757e5c3ec914f60ed1f6bce2819b6810953f12b8ba1199bf82d746a8b8a88f1'
+ + '142002978ec4c35b95dc2c82990f9e847a0ab45f2ca72625f5190c820f29f3aa'
+ + 'f5f0b5572b06b70a144f2a240c3b3098d4831fa1ce1459f8d1df226a6a79b0ab'
+ + '41e91799ef31b5ff3d756c19126b19025858ee70fbd69f2be955cb011c005e31'
+ + '32b271b378f39b0cb594e95c99ce6ff17735a541891845bbf0450afcb4a850b9'
+ + '4ee90afb713ae7e01295c74381180a3816d7020d5a396c0d97aaa783eaabb6ec'
+ + '44d5111157f2212d1b1b8fca7893e8b520cd482418c272ab119b569a2b9598eb'
+ + '355624d12e79adab81153b58cd22eaf1b2a32395dedc4a1c66f4d274070b9800'
+ + 'ea95766f0245a8295f8aadb36ddbbdfa936417c8dbc6235d19494036964d3e70'
+ + 'b125b0f800c3d53881d9d11e7970f827c2f9556935cd29e927b0aceb8cae5fd4'
+ + '0fd88a8854010a33db94c96c98735858f1c5df6844f864feaca8f41539313e7f'
+ + '3c0610214912cd5e6362197646207e2d64cd5b26c9dfe0822629dcbeb16662e8'
+ + '9ff5bf5cf2e499138a5e27bd5027329d0e68ddf53103e9e409523662e27f61f6'
+ + '5cf38c1232023e6a6ef66c315bcb2a4328642faabb7ca1e889e039e7c444b34b'
+ + 'b3443f596ac730f3df3dfcdb343c307c80f76e43e8898c5e8f43dc3bb280add0',
+ '0909090909090909090909090909090909090909090909090909090909090909',
+ '256 bits key, set 2, vector 9',
+ dict (iv='00'*8)),
+
+ ( '00' * 1024,
+ '71daee5142d0728b41b6597933ebf467e43279e30978677078941602629cbf68'
+ + 'b73d6bd2c95f118d2b3e6ec955dabb6dc61c4143bc9a9b32b99dbe6866166dc0'
+ + '8631b7d6553050303d7252c264d3a90d26c853634813e09ad7545a6ce7e84a5d'
+ + 'fc75ec43431207d5319970b0faadb0e1510625bb54372c8515e28e2accf0a993'
+ + '0ad15f431874923d2a59e20d9f2a5367dba6051564f150287debb1db536ff9b0'
+ + '9ad981f25e5010d85d76ee0c305f755b25e6f09341e0812f95c94f42eead346e'
+ + '81f39c58c5faa2c88953dc0cac90469db2063cb5cdb22c9eae22afbf0506fca4'
+ + '1dc710b846fbdfe3c46883dd118f3a5e8b11b6afd9e71680d8666557301a2daa'
+ + 'fb9496c559784d35a035360885f9b17bd7191977deea932b981ebdb29057ae3c'
+ + '92cfeff5e6c5d0cb62f209ce342d4e35c69646ccd14e53350e488bb310a32f8b'
+ + '0248e70acc5b473df537ced3f81a014d4083932bedd62ed0e447b6766cd2604b'
+ + '706e9b346c4468beb46a34ecf1610ebd38331d52bf33346afec15eefb2a7699e'
+ + '8759db5a1f636a48a039688e39de34d995df9f27ed9edc8dd795e39e53d9d925'
+ + 'b278010565ff665269042f05096d94da3433d957ec13d2fd82a0066283d0d1ee'
+ + 'b81bf0ef133b7fd90248b8ffb499b2414cd4fa003093ff0864575a43749bf596'
+ + '02f26c717fa96b1d057697db08ebc3fa664a016a67dcef8807577cc3a09385d3'
+ + 'f4dc79b34364bb3b166ce65fe1dd28e3950fe6fa81063f7b16ce1c0e6daac1f8'
+ + '188455b77752045e863c9b256ad92bc6e2d08314c5bba191c274f42dfbb3d652'
+ + 'bb771956555e880f84cd8b827a4c5a52f3a099fa0259bd4aac3efd541f191170'
+ + '4412d6e85fbcc628b335875b9fef24807f6e1bc66c3186159e1e7f5a13913e02'
+ + 'd241ce2efdbcaa275039fb14eac5923d17ffbc7f1abd3b45e92127575bfbabf9'
+ + '3a257ebef0aa1437b326e41b585af572f7239c33b32981a1577a4f629b027e1e'
+ + 'b49d58cc497e944d79cef44357c2bf25442ab779651e991147bf79d6fd3a8868'
+ + '0cd3b1748e07fd10d78aceef6db8a5e563570d40127f754146c34a440f2a991a'
+ + '23fa39d365141f255041f2135c5cba4373452c114da1801bacca38610e3a6524'
+ + '2b822d32de4ab5a7d3cf9b61b37493c863bd12e2cae10530cddcda2cb7a5436b'
+ + 'ef8988d4d24e8cdc31b2d2a3586340bc5141f8f6632d0dd543bfed81eb471ba1'
+ + 'f3dc2225a15ffddcc03eb48f44e27e2aa390598adf83f15c6608a5f18d4dfcf0'
+ + 'f547d467a4d70b281c83a595d7660d0b62de78b9cca023cca89d7b1f83484638'
+ + '0e228c25f049184a612ef5bb3d37454e6cfa5b10dceda619d898a699b3c8981a'
+ + '173407844bb89b4287bf57dd6600c79e352c681d74b03fa7ea0d7bf6ad69f8a6'
+ + '8ecb001963bd2dd8a2baa0083ec09751cd9742402ad716be16d5c052304cfca1',
+ '0F62B5085BAE0154A7FA4DA0F34699EC',
+ '128 bits key, Set 6, vector# 3',
+ dict (iv='288FF65DC42B92F9')),
+
+ ( '00' * 1024,
+ '5e5e71f90199340304abb22a37b6625bf883fb89ce3b21f54a10b81066ef87da'
+ + '30b77699aa7379da595c77dd59542da208e5954f89e40eb7aa80a84a6176663f'
+ + 'd910cde567cf1ff60f7040548d8f376bfd1f44c4774aac37410ede7d5c3463fc'
+ + '4508a603201d8495ad257894e5eb1914b53e8da5e4bf2bc83ac87ce55cc67df7'
+ + '093d9853d2a83a9c8be969175df7c807a17156df768445dd0874a9271c6537f5'
+ + 'ce0466473582375f067fa4fcdaf65dbc0139cd75e8c21a482f28c0fb8c3d9f94'
+ + '22606cc8e88fe28fe73ec3cb10ff0e8cc5f2a49e540f007265c65b7130bfdb98'
+ + '795b1df9522da46e48b30e55d9f0d787955ece720205b29c85f3ad9be33b4459'
+ + '7d21b54d06c9a60b04b8e640c64e566e51566730e86cf128ab14174f91bd8981'
+ + 'a6fb00fe587bbd6c38b5a1dfdb04ea7e61536fd229f957aa9b070ca931358e85'
+ + '11b92c53c523cb54828fb1513c5636fa9a0645b4a3c922c0db94986d92f314ff'
+ + '7852c03b231e4dceea5dd8cced621869cff818daf3c270ff3c8be2e5c74be767'
+ + 'a4e1fdf3327a934fe31e46df5a74ae2021cee021d958c4f615263d99a5ddae7f'
+ + 'eab45e6eccbafefe4761c57750847b7e75ee2e2f14333c0779ce4678f47b1e1b'
+ + '760a03a5f17d6e91d4b42313b3f1077ee270e432fe04917ed1fc8babebf7c941'
+ + '42b80dfb44a28a2a3e59093027606f6860bfb8c2e5897078cfccda7314c70035'
+ + 'f137de6f05daa035891d5f6f76e1df0fce1112a2ff0ac2bd3534b5d1bf4c7165'
+ + 'fb40a1b6eacb7f295711c4907ae457514a7010f3a342b4427593d61ba993bc59'
+ + '8bd09c56b9ee53aac5dd861fa4b4bb53888952a4aa9d8ca8671582de716270e1'
+ + '97375b3ee49e51fa2bf4ef32015dd9a764d966aa2ae541592d0aa650849e99ca'
+ + '5c6c39beebf516457cc32fe4c105bff314a12f1ec94bdf4d626f5d9b1cbbde42'
+ + 'e5733f0885765ba29e2e82c829d312f5fc7e180679ac84826c08d0a644b326d0'
+ + '44da0fdcc75fa53cfe4ced0437fa4df5a7ecbca8b4cb7c4a9ecf9a60d00a56eb'
+ + '81da52adc21f508dbb60a9503a3cc94a896616d86020d5b0e5c637329b6d396a'
+ + '41a21ba2c4a9493cf33fa2d4f10f77d5b12fdad7e478ccfe79b74851fc96a7ca'
+ + '6320c5efd561a222c0ab0fb44bbda0e42149611d2262bb7d1719150fa798718a'
+ + '0eec63ee297cad459869c8b0f06c4e2b56cbac03cd2605b2a924efedf85ec8f1'
+ + '9b0b6c90e7cbd933223ffeb1b3a3f9677657905829294c4c70acdb8b0891b47d'
+ + '0875d0cd6c0f4efe2917fc44b581ef0d1e4280197065d07da34ab33283364552'
+ + 'efad0bd9257b059acdd0a6f246812feb69e7e76065f27dbc2eee94da9cc41835'
+ + 'bf826e36e5cebe5d4d6a37a6a666246290ce51a0c082718ab0ec855668db1add'
+ + 'a658e5f257e0db39384d02e6145c4c00eaa079098f6d820d872de711b6ed08cf',
+ '0F62B5085BAE0154A7FA4DA0F34699EC3F92E5388BDE3184D72A7DD02376C91C',
+ '256 bits key, Set 6, vector# 3',
+ dict (iv='288FF65DC42B92F9')),
+
+]
+
+
+class KeyLength(unittest.TestCase):
+
+ def runTest(self):
+
+ nonce = bchr(0) * 8
+ for key_length in (15, 30, 33):
+ key = bchr(1) * key_length
+ self.assertRaises(ValueError, Salsa20.new, key, nonce)
+
+
+class NonceTests(unittest.TestCase):
+
+ def test_invalid_nonce_length(self):
+ key = bchr(1) * 16
+ self.assertRaises(ValueError, Salsa20.new, key, bchr(0) * 7)
+ self.assertRaises(ValueError, Salsa20.new, key, bchr(0) * 9)
+
+ def test_default_nonce(self):
+
+ cipher1 = Salsa20.new(bchr(1) * 16)
+ cipher2 = Salsa20.new(bchr(1) * 16)
+ self.assertEqual(len(cipher1.nonce), 8)
+ self.assertNotEqual(cipher1.nonce, cipher2.nonce)
+
+
+class ByteArrayTest(unittest.TestCase):
+ """Verify we can encrypt or decrypt bytearrays"""
+
+ def runTest(self):
+
+ data = b"0123"
+ key = b"9" * 32
+ nonce = b"t" * 8
+
+ # Encryption
+ data_ba = bytearray(data)
+ key_ba = bytearray(key)
+ nonce_ba = bytearray(nonce)
+
+ cipher1 = Salsa20.new(key=key, nonce=nonce)
+ ct = cipher1.encrypt(data)
+
+ cipher2 = Salsa20.new(key=key_ba, nonce=nonce_ba)
+ key_ba[:1] = b'\xFF'
+ nonce_ba[:1] = b'\xFF'
+ ct_test = cipher2.encrypt(data_ba)
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decryption
+ key_ba = bytearray(key)
+ nonce_ba = bytearray(nonce)
+ ct_ba = bytearray(ct)
+
+ cipher3 = Salsa20.new(key=key_ba, nonce=nonce_ba)
+ key_ba[:1] = b'\xFF'
+ nonce_ba[:1] = b'\xFF'
+ pt_test = cipher3.decrypt(ct_ba)
+
+ self.assertEqual(data, pt_test)
+
+
+class MemoryviewTest(unittest.TestCase):
+ """Verify we can encrypt or decrypt bytearrays"""
+
+ def runTest(self):
+
+ data = b"0123"
+ key = b"9" * 32
+ nonce = b"t" * 8
+
+ # Encryption
+ data_mv = memoryview(bytearray(data))
+ key_mv = memoryview(bytearray(key))
+ nonce_mv = memoryview(bytearray(nonce))
+
+ cipher1 = Salsa20.new(key=key, nonce=nonce)
+ ct = cipher1.encrypt(data)
+
+ cipher2 = Salsa20.new(key=key_mv, nonce=nonce_mv)
+ key_mv[:1] = b'\xFF'
+ nonce_mv[:1] = b'\xFF'
+ ct_test = cipher2.encrypt(data_mv)
+
+ self.assertEqual(ct, ct_test)
+ self.assertEqual(cipher1.nonce, cipher2.nonce)
+
+ # Decryption
+ key_mv = memoryview(bytearray(key))
+ nonce_mv = memoryview(bytearray(nonce))
+ ct_mv = memoryview(bytearray(ct))
+
+ cipher3 = Salsa20.new(key=key_mv, nonce=nonce_mv)
+ key_mv[:1] = b'\xFF'
+ nonce_mv[:1] = b'\xFF'
+ pt_test = cipher3.decrypt(ct_mv)
+
+ self.assertEqual(data, pt_test)
+
+
+class TestOutput(unittest.TestCase):
+
+ def runTest(self):
+ # Encrypt/Decrypt data and test output parameter
+
+ key = b'4' * 32
+ nonce = b'5' * 8
+ cipher = Salsa20.new(key=key, nonce=nonce)
+
+ pt = b'5' * 300
+ ct = cipher.encrypt(pt)
+
+ output = bytearray(len(pt))
+ cipher = Salsa20.new(key=key, nonce=nonce)
+ res = cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+ self.assertEqual(res, None)
+
+ cipher = Salsa20.new(key=key, nonce=nonce)
+ res = cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+ self.assertEqual(res, None)
+
+ output = memoryview(bytearray(len(pt)))
+ cipher = Salsa20.new(key=key, nonce=nonce)
+ cipher.encrypt(pt, output=output)
+ self.assertEqual(ct, output)
+
+ cipher = Salsa20.new(key=key, nonce=nonce)
+ cipher.decrypt(ct, output=output)
+ self.assertEqual(pt, output)
+
+ cipher = Salsa20.new(key=key, nonce=nonce)
+ self.assertRaises(TypeError, cipher.encrypt, pt, output=b'0'*len(pt))
+
+ cipher = Salsa20.new(key=key, nonce=nonce)
+ self.assertRaises(TypeError, cipher.decrypt, ct, output=b'0'*len(ct))
+
+ shorter_output = bytearray(len(pt) - 1)
+
+ cipher = Salsa20.new(key=key, nonce=nonce)
+ self.assertRaises(ValueError, cipher.encrypt, pt, output=shorter_output)
+
+ cipher = Salsa20.new(key=key, nonce=nonce)
+ self.assertRaises(ValueError, cipher.decrypt, ct, output=shorter_output)
+
+
+def get_tests(config={}):
+ tests = make_stream_tests(Salsa20, "Salsa20", test_data)
+ tests.append(KeyLength())
+ tests += list_test_cases(NonceTests)
+ tests.append(ByteArrayTest())
+ tests.append(MemoryviewTest())
+ tests.append(TestOutput())
+
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_pkcs1_15.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_pkcs1_15.py
new file mode 100644
index 00000000..e16a5438
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_pkcs1_15.py
@@ -0,0 +1,283 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/test_pkcs1_15.py: Self-test for PKCS#1 v1.5 encryption
+#
+# ===================================================================
+# 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 __future__ import print_function
+
+import unittest
+
+from Crypto.PublicKey import RSA
+from Crypto.SelfTest.st_common import list_test_cases, a2b_hex
+from Crypto import Random
+from Crypto.Cipher import PKCS1_v1_5 as PKCS
+from Crypto.Util.py3compat import b
+from Crypto.Util.number import bytes_to_long, long_to_bytes
+from Crypto.SelfTest.loader import load_test_vectors_wycheproof
+
+
+def rws(t):
+ """Remove white spaces, tabs, and new lines from a string"""
+ for c in ['\n', '\t', ' ']:
+ t = t.replace(c, '')
+ return t
+
+
+def t2b(t):
+ """Convert a text string with bytes in hex form to a byte string"""
+ clean = b(rws(t))
+ if len(clean) % 2 == 1:
+ raise ValueError("Even number of characters expected")
+ return a2b_hex(clean)
+
+
+class PKCS1_15_Tests(unittest.TestCase):
+
+ def setUp(self):
+ self.rng = Random.new().read
+ self.key1024 = RSA.generate(1024, self.rng)
+
+ # List of tuples with test data for PKCS#1 v1.5.
+ # Each tuple is made up by:
+ # Item #0: dictionary with RSA key component, or key to import
+ # Item #1: plaintext
+ # Item #2: ciphertext
+ # Item #3: random data
+
+ _testData = (
+
+ #
+ # Generated with openssl 0.9.8o
+ #
+ (
+ # Private key
+ '''-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDAiAnvIAOvqVwJTaYzsKnefZftgtXGE2hPJppGsWl78yz9jeXY
+W/FxX/gTPURArNhdnhP6n3p2ZaDIBrO2zizbgIXs0IsljTTcr4vnI8fMXzyNUOjA
+zP3nzMqZDZK6757XQAobOssMkBFqRWwilT/3DsBhRpl3iMUhF+wvpTSHewIDAQAB
+AoGAC4HV/inOrpgTvSab8Wj0riyZgQOZ3U3ZpSlsfR8ra9Ib9Uee3jCYnKscu6Gk
+y6zI/cdt8EPJ4PuwAWSNJzbpbVaDvUq25OD+CX8/uRT08yBS4J8TzBitZJTD4lS7
+atdTnKT0Wmwk+u8tDbhvMKwnUHdJLcuIsycts9rwJVapUtkCQQDvDpx2JMun0YKG
+uUttjmL8oJ3U0m3ZvMdVwBecA0eebZb1l2J5PvI3EJD97eKe91Nsw8T3lwpoN40k
+IocSVDklAkEAzi1HLHE6EzVPOe5+Y0kGvrIYRRhncOb72vCvBZvD6wLZpQgqo6c4
+d3XHFBBQWA6xcvQb5w+VVEJZzw64y25sHwJBAMYReRl6SzL0qA0wIYrYWrOt8JeQ
+8mthulcWHXmqTgC6FEXP9Es5GD7/fuKl4wqLKZgIbH4nqvvGay7xXLCXD/ECQH9a
+1JYNMtRen5unSAbIOxRcKkWz92F0LKpm9ZW/S9vFHO+mBcClMGoKJHiuQxLBsLbT
+NtEZfSJZAeS2sUtn3/0CQDb2M2zNBTF8LlM0nxmh0k9VGm5TVIyBEMcipmvOgqIs
+HKukWBcq9f/UOmS0oEhai/6g+Uf7VHJdWaeO5LzuvwU=
+-----END RSA PRIVATE KEY-----''',
+ # Plaintext
+ '''THIS IS PLAINTEXT\x0A''',
+ # Ciphertext
+ '''3f dc fd 3c cd 5c 9b 12 af 65 32 e3 f7 d0 da 36
+ 8f 8f d9 e3 13 1c 7f c8 b3 f9 c1 08 e4 eb 79 9c
+ 91 89 1f 96 3b 94 77 61 99 a4 b1 ee 5d e6 17 c9
+ 5d 0a b5 63 52 0a eb 00 45 38 2a fb b0 71 3d 11
+ f7 a1 9e a7 69 b3 af 61 c0 bb 04 5b 5d 4b 27 44
+ 1f 5b 97 89 ba 6a 08 95 ee 4f a2 eb 56 64 e5 0f
+ da 7c f9 9a 61 61 06 62 ed a0 bc 5f aa 6c 31 78
+ 70 28 1a bb 98 3c e3 6a 60 3c d1 0b 0f 5a f4 75''',
+ # Random data
+ '''eb d7 7d 86 a4 35 23 a3 54 7e 02 0b 42 1d
+ 61 6c af 67 b8 4e 17 56 80 66 36 04 64 34 26 8a
+ 47 dd 44 b3 1a b2 17 60 f4 91 2e e2 b5 95 64 cc
+ f9 da c8 70 94 54 86 4c ef 5b 08 7d 18 c4 ab 8d
+ 04 06 33 8f ca 15 5f 52 60 8a a1 0c f5 08 b5 4c
+ bb 99 b8 94 25 04 9c e6 01 75 e6 f9 63 7a 65 61
+ 13 8a a7 47 77 81 ae 0d b8 2c 4d 50 a5'''
+ ),
+ )
+
+ def testEncrypt1(self):
+ for test in self._testData:
+ # Build the key
+ key = RSA.importKey(test[0])
+ # RNG that takes its random numbers from a pool given
+ # at initialization
+ class randGen:
+ def __init__(self, data):
+ self.data = data
+ self.idx = 0
+ def __call__(self, N):
+ r = self.data[self.idx:self.idx+N]
+ self.idx += N
+ return r
+ # The real test
+ cipher = PKCS.new(key, randfunc=randGen(t2b(test[3])))
+ ct = cipher.encrypt(b(test[1]))
+ self.assertEqual(ct, t2b(test[2]))
+
+ def testEncrypt2(self):
+ # Verify that encryption fail if plaintext is too long
+ pt = '\x00'*(128-11+1)
+ cipher = PKCS.new(self.key1024)
+ self.assertRaises(ValueError, cipher.encrypt, pt)
+
+ def testVerify1(self):
+ for test in self._testData:
+ key = RSA.importKey(test[0])
+ expected_pt = b(test[1])
+ ct = t2b(test[2])
+ cipher = PKCS.new(key)
+
+ # The real test
+ pt = cipher.decrypt(ct, None)
+ self.assertEqual(pt, expected_pt)
+
+ pt = cipher.decrypt(ct, b'\xFF' * len(expected_pt))
+ self.assertEqual(pt, expected_pt)
+
+ def testVerify2(self):
+ # Verify that decryption fails if ciphertext is not as long as
+ # RSA modulus
+ cipher = PKCS.new(self.key1024)
+ self.assertRaises(ValueError, cipher.decrypt, '\x00'*127, "---")
+ self.assertRaises(ValueError, cipher.decrypt, '\x00'*129, "---")
+
+ # Verify that decryption fails if there are less then 8 non-zero padding
+ # bytes
+ pt = b('\x00\x02' + '\xFF'*7 + '\x00' + '\x45'*118)
+ pt_int = bytes_to_long(pt)
+ ct_int = self.key1024._encrypt(pt_int)
+ ct = long_to_bytes(ct_int, 128)
+ self.assertEqual(b"---", cipher.decrypt(ct, b"---"))
+
+ def testEncryptVerify1(self):
+ # Encrypt/Verify messages of length [0..RSAlen-11]
+ # and therefore padding [8..117]
+ for pt_len in range(0, 128 - 11 + 1):
+ pt = self.rng(pt_len)
+ cipher = PKCS.new(self.key1024)
+ ct = cipher.encrypt(pt)
+ pt2 = cipher.decrypt(ct, b'\xAA' * pt_len)
+ self.assertEqual(pt, pt2)
+
+ def test_encrypt_verify_exp_pt_len(self):
+
+ cipher = PKCS.new(self.key1024)
+ pt = b'5' * 16
+ ct = cipher.encrypt(pt)
+ sentinel = b'\xAA' * 16
+
+ pt_A = cipher.decrypt(ct, sentinel, 16)
+ self.assertEqual(pt, pt_A)
+
+ pt_B = cipher.decrypt(ct, sentinel, 15)
+ self.assertEqual(sentinel, pt_B)
+
+ pt_C = cipher.decrypt(ct, sentinel, 17)
+ self.assertEqual(sentinel, pt_C)
+
+ def testByteArray(self):
+ pt = b"XER"
+ cipher = PKCS.new(self.key1024)
+ ct = cipher.encrypt(bytearray(pt))
+ pt2 = cipher.decrypt(bytearray(ct), '\xFF' * len(pt))
+ self.assertEqual(pt, pt2)
+
+ def testMemoryview(self):
+ pt = b"XER"
+ cipher = PKCS.new(self.key1024)
+ ct = cipher.encrypt(memoryview(bytearray(pt)))
+ pt2 = cipher.decrypt(memoryview(bytearray(ct)), b'\xFF' * len(pt))
+ self.assertEqual(pt, pt2)
+
+ def test_return_type(self):
+ pt = b"XYZ"
+ cipher = PKCS.new(self.key1024)
+ ct = cipher.encrypt(pt)
+ self.assertTrue(isinstance(ct, bytes))
+ pt2 = cipher.decrypt(ct, b'\xAA' * 3)
+ self.assertTrue(isinstance(pt2, bytes))
+
+
+class TestVectorsWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings, skip_slow_tests):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._skip_slow_tests = skip_slow_tests
+ self._id = "None"
+
+ def load_tests(self, filename):
+
+ def filter_rsa(group):
+ return RSA.import_key(group['privateKeyPem'])
+
+ result = load_test_vectors_wycheproof(("Cipher", "wycheproof"),
+ filename,
+ "Wycheproof PKCS#1v1.5 (%s)" % filename,
+ group_tag={'rsa_key': filter_rsa}
+ )
+ return result
+
+ def setUp(self):
+ self.tv = []
+ self.tv.extend(self.load_tests("rsa_pkcs1_2048_test.json"))
+ if not self._skip_slow_tests:
+ self.tv.extend(self.load_tests("rsa_pkcs1_3072_test.json"))
+ self.tv.extend(self.load_tests("rsa_pkcs1_4096_test.json"))
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_decrypt(self, tv):
+ self._id = "Wycheproof Decrypt PKCS#1v1.5 Test #%s" % tv.id
+ sentinel = b'\xAA' * max(3, len(tv.msg))
+ cipher = PKCS.new(tv.rsa_key)
+ try:
+ pt = cipher.decrypt(tv.ct, sentinel=sentinel)
+ except ValueError:
+ assert not tv.valid
+ else:
+ if pt == sentinel:
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.assertEqual(pt, tv.msg)
+ self.warn(tv)
+
+ def runTest(self):
+
+ for tv in self.tv:
+ self.test_decrypt(tv)
+
+
+def get_tests(config={}):
+ skip_slow_tests = not config.get('slow_tests')
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ tests = []
+ tests += list_test_cases(PKCS1_15_Tests)
+ tests += [TestVectorsWycheproof(wycheproof_warnings, skip_slow_tests)]
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_pkcs1_oaep.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_pkcs1_oaep.py
new file mode 100644
index 00000000..17115819
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Cipher/test_pkcs1_oaep.py
@@ -0,0 +1,506 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Cipher/test_pkcs1_oaep.py: Self-test for PKCS#1 OAEP encryption
+#
+# ===================================================================
+# 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 unittest
+
+from Crypto.SelfTest.st_common import list_test_cases, a2b_hex
+from Crypto.SelfTest.loader import load_test_vectors_wycheproof
+
+from Crypto.PublicKey import RSA
+from Crypto.Cipher import PKCS1_OAEP as PKCS
+from Crypto.Hash import MD2, MD5, SHA1, SHA256, RIPEMD160, SHA224, SHA384, SHA512
+from Crypto import Random
+from Crypto.Signature.pss import MGF1
+
+from Crypto.Util.py3compat import b, bchr
+
+
+def rws(t):
+ """Remove white spaces, tabs, and new lines from a string"""
+ for c in ['\n', '\t', ' ']:
+ t = t.replace(c, '')
+ return t
+
+
+def t2b(t):
+ """Convert a text string with bytes in hex form to a byte string"""
+ clean = rws(t)
+ if len(clean) % 2 == 1:
+ raise ValueError("Even number of characters expected")
+ return a2b_hex(clean)
+
+
+class PKCS1_OAEP_Tests(unittest.TestCase):
+
+ def setUp(self):
+ self.rng = Random.new().read
+ self.key1024 = RSA.generate(1024, self.rng)
+
+ # List of tuples with test data for PKCS#1 OAEP
+ # Each tuple is made up by:
+ # Item #0: dictionary with RSA key component
+ # Item #1: plaintext
+ # Item #2: ciphertext
+ # Item #3: random data (=seed)
+ # Item #4: hash object
+
+ _testData = (
+
+ #
+ # From in oaep-int.txt to be found in
+ # ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1-vec.zip
+ #
+ (
+ # Private key
+ {
+ 'n':'''bb f8 2f 09 06 82 ce 9c 23 38 ac 2b 9d a8 71 f7
+ 36 8d 07 ee d4 10 43 a4 40 d6 b6 f0 74 54 f5 1f
+ b8 df ba af 03 5c 02 ab 61 ea 48 ce eb 6f cd 48
+ 76 ed 52 0d 60 e1 ec 46 19 71 9d 8a 5b 8b 80 7f
+ af b8 e0 a3 df c7 37 72 3e e6 b4 b7 d9 3a 25 84
+ ee 6a 64 9d 06 09 53 74 88 34 b2 45 45 98 39 4e
+ e0 aa b1 2d 7b 61 a5 1f 52 7a 9a 41 f6 c1 68 7f
+ e2 53 72 98 ca 2a 8f 59 46 f8 e5 fd 09 1d bd cb''',
+ # Public key
+ 'e':'11',
+ # In the test vector, only p and q were given...
+ # d is computed offline as e^{-1} mod (p-1)(q-1)
+ 'd':'''a5dafc5341faf289c4b988db30c1cdf83f31251e0
+ 668b42784813801579641b29410b3c7998d6bc465745e5c3
+ 92669d6870da2c082a939e37fdcb82ec93edac97ff3ad595
+ 0accfbc111c76f1a9529444e56aaf68c56c092cd38dc3bef
+ 5d20a939926ed4f74a13eddfbe1a1cecc4894af9428c2b7b
+ 8883fe4463a4bc85b1cb3c1'''
+ }
+ ,
+ # Plaintext
+ '''d4 36 e9 95 69 fd 32 a7 c8 a0 5b bc 90 d3 2c 49''',
+ # Ciphertext
+ '''12 53 e0 4d c0 a5 39 7b b4 4a 7a b8 7e 9b f2 a0
+ 39 a3 3d 1e 99 6f c8 2a 94 cc d3 00 74 c9 5d f7
+ 63 72 20 17 06 9e 52 68 da 5d 1c 0b 4f 87 2c f6
+ 53 c1 1d f8 23 14 a6 79 68 df ea e2 8d ef 04 bb
+ 6d 84 b1 c3 1d 65 4a 19 70 e5 78 3b d6 eb 96 a0
+ 24 c2 ca 2f 4a 90 fe 9f 2e f5 c9 c1 40 e5 bb 48
+ da 95 36 ad 87 00 c8 4f c9 13 0a de a7 4e 55 8d
+ 51 a7 4d df 85 d8 b5 0d e9 68 38 d6 06 3e 09 55''',
+ # Random
+ '''aa fd 12 f6 59 ca e6 34 89 b4 79 e5 07 6d de c2
+ f0 6c b5 8f''',
+ # Hash
+ SHA1,
+ ),
+
+ #
+ # From in oaep-vect.txt to be found in Example 1.1
+ # ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1-vec.zip
+ #
+ (
+ # Private key
+ {
+ 'n':'''a8 b3 b2 84 af 8e b5 0b 38 70 34 a8 60 f1 46 c4
+ 91 9f 31 87 63 cd 6c 55 98 c8 ae 48 11 a1 e0 ab
+ c4 c7 e0 b0 82 d6 93 a5 e7 fc ed 67 5c f4 66 85
+ 12 77 2c 0c bc 64 a7 42 c6 c6 30 f5 33 c8 cc 72
+ f6 2a e8 33 c4 0b f2 58 42 e9 84 bb 78 bd bf 97
+ c0 10 7d 55 bd b6 62 f5 c4 e0 fa b9 84 5c b5 14
+ 8e f7 39 2d d3 aa ff 93 ae 1e 6b 66 7b b3 d4 24
+ 76 16 d4 f5 ba 10 d4 cf d2 26 de 88 d3 9f 16 fb''',
+ 'e':'''01 00 01''',
+ 'd':'''53 33 9c fd b7 9f c8 46 6a 65 5c 73 16 ac a8 5c
+ 55 fd 8f 6d d8 98 fd af 11 95 17 ef 4f 52 e8 fd
+ 8e 25 8d f9 3f ee 18 0f a0 e4 ab 29 69 3c d8 3b
+ 15 2a 55 3d 4a c4 d1 81 2b 8b 9f a5 af 0e 7f 55
+ fe 73 04 df 41 57 09 26 f3 31 1f 15 c4 d6 5a 73
+ 2c 48 31 16 ee 3d 3d 2d 0a f3 54 9a d9 bf 7c bf
+ b7 8a d8 84 f8 4d 5b eb 04 72 4d c7 36 9b 31 de
+ f3 7d 0c f5 39 e9 cf cd d3 de 65 37 29 ea d5 d1 '''
+ }
+ ,
+ # Plaintext
+ '''66 28 19 4e 12 07 3d b0 3b a9 4c da 9e f9 53 23
+ 97 d5 0d ba 79 b9 87 00 4a fe fe 34''',
+ # Ciphertext
+ '''35 4f e6 7b 4a 12 6d 5d 35 fe 36 c7 77 79 1a 3f
+ 7b a1 3d ef 48 4e 2d 39 08 af f7 22 fa d4 68 fb
+ 21 69 6d e9 5d 0b e9 11 c2 d3 17 4f 8a fc c2 01
+ 03 5f 7b 6d 8e 69 40 2d e5 45 16 18 c2 1a 53 5f
+ a9 d7 bf c5 b8 dd 9f c2 43 f8 cf 92 7d b3 13 22
+ d6 e8 81 ea a9 1a 99 61 70 e6 57 a0 5a 26 64 26
+ d9 8c 88 00 3f 84 77 c1 22 70 94 a0 d9 fa 1e 8c
+ 40 24 30 9c e1 ec cc b5 21 00 35 d4 7a c7 2e 8a''',
+ # Random
+ '''18 b7 76 ea 21 06 9d 69 77 6a 33 e9 6b ad 48 e1
+ dd a0 a5 ef''',
+ SHA1
+ ),
+
+ #
+ # From in oaep-vect.txt to be found in Example 2.1
+ # ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1-vec.zip
+ #
+ (
+ # Private key
+ {
+ 'n':'''01 94 7c 7f ce 90 42 5f 47 27 9e 70 85 1f 25 d5
+ e6 23 16 fe 8a 1d f1 93 71 e3 e6 28 e2 60 54 3e
+ 49 01 ef 60 81 f6 8c 0b 81 41 19 0d 2a e8 da ba
+ 7d 12 50 ec 6d b6 36 e9 44 ec 37 22 87 7c 7c 1d
+ 0a 67 f1 4b 16 94 c5 f0 37 94 51 a4 3e 49 a3 2d
+ de 83 67 0b 73 da 91 a1 c9 9b c2 3b 43 6a 60 05
+ 5c 61 0f 0b af 99 c1 a0 79 56 5b 95 a3 f1 52 66
+ 32 d1 d4 da 60 f2 0e da 25 e6 53 c4 f0 02 76 6f
+ 45''',
+ 'e':'''01 00 01''',
+ 'd':'''08 23 f2 0f ad b5 da 89 08 8a 9d 00 89 3e 21 fa
+ 4a 1b 11 fb c9 3c 64 a3 be 0b aa ea 97 fb 3b 93
+ c3 ff 71 37 04 c1 9c 96 3c 1d 10 7a ae 99 05 47
+ 39 f7 9e 02 e1 86 de 86 f8 7a 6d de fe a6 d8 cc
+ d1 d3 c8 1a 47 bf a7 25 5b e2 06 01 a4 a4 b2 f0
+ 8a 16 7b 5e 27 9d 71 5b 1b 45 5b dd 7e ab 24 59
+ 41 d9 76 8b 9a ce fb 3c cd a5 95 2d a3 ce e7 25
+ 25 b4 50 16 63 a8 ee 15 c9 e9 92 d9 24 62 fe 39'''
+ },
+ # Plaintext
+ '''8f f0 0c aa 60 5c 70 28 30 63 4d 9a 6c 3d 42 c6
+ 52 b5 8c f1 d9 2f ec 57 0b ee e7''',
+ # Ciphertext
+ '''01 81 af 89 22 b9 fc b4 d7 9d 92 eb e1 98 15 99
+ 2f c0 c1 43 9d 8b cd 49 13 98 a0 f4 ad 3a 32 9a
+ 5b d9 38 55 60 db 53 26 83 c8 b7 da 04 e4 b1 2a
+ ed 6a ac df 47 1c 34 c9 cd a8 91 ad dc c2 df 34
+ 56 65 3a a6 38 2e 9a e5 9b 54 45 52 57 eb 09 9d
+ 56 2b be 10 45 3f 2b 6d 13 c5 9c 02 e1 0f 1f 8a
+ bb 5d a0 d0 57 09 32 da cf 2d 09 01 db 72 9d 0f
+ ef cc 05 4e 70 96 8e a5 40 c8 1b 04 bc ae fe 72
+ 0e''',
+ # Random
+ '''8c 40 7b 5e c2 89 9e 50 99 c5 3e 8c e7 93 bf 94
+ e7 1b 17 82''',
+ SHA1
+ ),
+
+ #
+ # From in oaep-vect.txt to be found in Example 10.1
+ # ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1-vec.zip
+ #
+ (
+ # Private key
+ {
+ 'n':'''ae 45 ed 56 01 ce c6 b8 cc 05 f8 03 93 5c 67 4d
+ db e0 d7 5c 4c 09 fd 79 51 fc 6b 0c ae c3 13 a8
+ df 39 97 0c 51 8b ff ba 5e d6 8f 3f 0d 7f 22 a4
+ 02 9d 41 3f 1a e0 7e 4e be 9e 41 77 ce 23 e7 f5
+ 40 4b 56 9e 4e e1 bd cf 3c 1f b0 3e f1 13 80 2d
+ 4f 85 5e b9 b5 13 4b 5a 7c 80 85 ad ca e6 fa 2f
+ a1 41 7e c3 76 3b e1 71 b0 c6 2b 76 0e de 23 c1
+ 2a d9 2b 98 08 84 c6 41 f5 a8 fa c2 6b da d4 a0
+ 33 81 a2 2f e1 b7 54 88 50 94 c8 25 06 d4 01 9a
+ 53 5a 28 6a fe b2 71 bb 9b a5 92 de 18 dc f6 00
+ c2 ae ea e5 6e 02 f7 cf 79 fc 14 cf 3b dc 7c d8
+ 4f eb bb f9 50 ca 90 30 4b 22 19 a7 aa 06 3a ef
+ a2 c3 c1 98 0e 56 0c d6 4a fe 77 95 85 b6 10 76
+ 57 b9 57 85 7e fd e6 01 09 88 ab 7d e4 17 fc 88
+ d8 f3 84 c4 e6 e7 2c 3f 94 3e 0c 31 c0 c4 a5 cc
+ 36 f8 79 d8 a3 ac 9d 7d 59 86 0e aa da 6b 83 bb''',
+ 'e':'''01 00 01''',
+ 'd':'''05 6b 04 21 6f e5 f3 54 ac 77 25 0a 4b 6b 0c 85
+ 25 a8 5c 59 b0 bd 80 c5 64 50 a2 2d 5f 43 8e 59
+ 6a 33 3a a8 75 e2 91 dd 43 f4 8c b8 8b 9d 5f c0
+ d4 99 f9 fc d1 c3 97 f9 af c0 70 cd 9e 39 8c 8d
+ 19 e6 1d b7 c7 41 0a 6b 26 75 df bf 5d 34 5b 80
+ 4d 20 1a dd 50 2d 5c e2 df cb 09 1c e9 99 7b be
+ be 57 30 6f 38 3e 4d 58 81 03 f0 36 f7 e8 5d 19
+ 34 d1 52 a3 23 e4 a8 db 45 1d 6f 4a 5b 1b 0f 10
+ 2c c1 50 e0 2f ee e2 b8 8d ea 4a d4 c1 ba cc b2
+ 4d 84 07 2d 14 e1 d2 4a 67 71 f7 40 8e e3 05 64
+ fb 86 d4 39 3a 34 bc f0 b7 88 50 1d 19 33 03 f1
+ 3a 22 84 b0 01 f0 f6 49 ea f7 93 28 d4 ac 5c 43
+ 0a b4 41 49 20 a9 46 0e d1 b7 bc 40 ec 65 3e 87
+ 6d 09 ab c5 09 ae 45 b5 25 19 01 16 a0 c2 61 01
+ 84 82 98 50 9c 1c 3b f3 a4 83 e7 27 40 54 e1 5e
+ 97 07 50 36 e9 89 f6 09 32 80 7b 52 57 75 1e 79'''
+ },
+ # Plaintext
+ '''8b ba 6b f8 2a 6c 0f 86 d5 f1 75 6e 97 95 68 70
+ b0 89 53 b0 6b 4e b2 05 bc 16 94 ee''',
+ # Ciphertext
+ '''53 ea 5d c0 8c d2 60 fb 3b 85 85 67 28 7f a9 15
+ 52 c3 0b 2f eb fb a2 13 f0 ae 87 70 2d 06 8d 19
+ ba b0 7f e5 74 52 3d fb 42 13 9d 68 c3 c5 af ee
+ e0 bf e4 cb 79 69 cb f3 82 b8 04 d6 e6 13 96 14
+ 4e 2d 0e 60 74 1f 89 93 c3 01 4b 58 b9 b1 95 7a
+ 8b ab cd 23 af 85 4f 4c 35 6f b1 66 2a a7 2b fc
+ c7 e5 86 55 9d c4 28 0d 16 0c 12 67 85 a7 23 eb
+ ee be ff 71 f1 15 94 44 0a ae f8 7d 10 79 3a 87
+ 74 a2 39 d4 a0 4c 87 fe 14 67 b9 da f8 52 08 ec
+ 6c 72 55 79 4a 96 cc 29 14 2f 9a 8b d4 18 e3 c1
+ fd 67 34 4b 0c d0 82 9d f3 b2 be c6 02 53 19 62
+ 93 c6 b3 4d 3f 75 d3 2f 21 3d d4 5c 62 73 d5 05
+ ad f4 cc ed 10 57 cb 75 8f c2 6a ee fa 44 12 55
+ ed 4e 64 c1 99 ee 07 5e 7f 16 64 61 82 fd b4 64
+ 73 9b 68 ab 5d af f0 e6 3e 95 52 01 68 24 f0 54
+ bf 4d 3c 8c 90 a9 7b b6 b6 55 32 84 eb 42 9f cc''',
+ # Random
+ '''47 e1 ab 71 19 fe e5 6c 95 ee 5e aa d8 6f 40 d0
+ aa 63 bd 33''',
+ SHA1
+ ),
+ )
+
+ def testEncrypt1(self):
+ # Verify encryption using all test vectors
+ for test in self._testData:
+ # Build the key
+ comps = [int(rws(test[0][x]), 16) for x in ('n', 'e')]
+ key = RSA.construct(comps)
+
+ # RNG that takes its random numbers from a pool given
+ # at initialization
+ class randGen:
+
+ def __init__(self, data):
+ self.data = data
+ self.idx = 0
+
+ def __call__(self, N):
+ r = self.data[self.idx:N]
+ self.idx += N
+ return r
+
+ # The real test
+ cipher = PKCS.new(key, test[4], randfunc=randGen(t2b(test[3])))
+ ct = cipher.encrypt(t2b(test[1]))
+ self.assertEqual(ct, t2b(test[2]))
+
+ def testEncrypt2(self):
+ # Verify that encryption fails if plaintext is too long
+ pt = '\x00'*(128-2*20-2+1)
+ cipher = PKCS.new(self.key1024)
+ self.assertRaises(ValueError, cipher.encrypt, pt)
+
+ def testDecrypt1(self):
+ # Verify decryption using all test vectors
+ for test in self._testData:
+ # Build the key
+ comps = [int(rws(test[0][x]),16) for x in ('n', 'e', 'd')]
+ key = RSA.construct(comps)
+ # The real test
+ cipher = PKCS.new(key, test[4])
+ pt = cipher.decrypt(t2b(test[2]))
+ self.assertEqual(pt, t2b(test[1]))
+
+ def testDecrypt2(self):
+ # Simplest possible negative tests
+ for ct_size in (127, 128, 129):
+ cipher = PKCS.new(self.key1024)
+ self.assertRaises(ValueError, cipher.decrypt, bchr(0x00)*ct_size)
+
+ def testEncryptDecrypt1(self):
+ # Encrypt/Decrypt messages of length [0..128-2*20-2]
+ for pt_len in range(0, 128-2*20-2):
+ pt = self.rng(pt_len)
+ cipher = PKCS.new(self.key1024)
+ ct = cipher.encrypt(pt)
+ pt2 = cipher.decrypt(ct)
+ self.assertEqual(pt, pt2)
+
+ def testEncryptDecrypt2(self):
+ # Helper function to monitor what's requested from RNG
+ global asked
+
+ def localRng(N):
+ global asked
+ asked += N
+ return self.rng(N)
+
+ # Verify that OAEP is friendly to all hashes
+ for hashmod in (MD2, MD5, SHA1, SHA256, RIPEMD160):
+ # Verify that encrypt() asks for as many random bytes
+ # as the hash output size
+ asked = 0
+ pt = self.rng(40)
+ cipher = PKCS.new(self.key1024, hashmod, randfunc=localRng)
+ ct = cipher.encrypt(pt)
+ self.assertEqual(cipher.decrypt(ct), pt)
+ self.assertEqual(asked, hashmod.digest_size)
+
+ def testEncryptDecrypt3(self):
+ # Verify that OAEP supports labels
+ pt = self.rng(35)
+ xlabel = self.rng(22)
+ cipher = PKCS.new(self.key1024, label=xlabel)
+ ct = cipher.encrypt(pt)
+ self.assertEqual(cipher.decrypt(ct), pt)
+
+ def testEncryptDecrypt4(self):
+ # Verify that encrypt() uses the custom MGF
+ global mgfcalls
+ # Helper function to monitor what's requested from MGF
+
+ def newMGF(seed, maskLen):
+ global mgfcalls
+ mgfcalls += 1
+ return b'\x00' * maskLen
+
+ mgfcalls = 0
+ pt = self.rng(32)
+ cipher = PKCS.new(self.key1024, mgfunc=newMGF)
+ ct = cipher.encrypt(pt)
+ self.assertEqual(mgfcalls, 2)
+ self.assertEqual(cipher.decrypt(ct), pt)
+
+ def testByteArray(self):
+ pt = b("XER")
+ cipher = PKCS.new(self.key1024)
+ ct = cipher.encrypt(bytearray(pt))
+ pt2 = cipher.decrypt(bytearray(ct))
+ self.assertEqual(pt, pt2)
+
+ def testMemoryview(self):
+ pt = b("XER")
+ cipher = PKCS.new(self.key1024)
+ ct = cipher.encrypt(memoryview(bytearray(pt)))
+ pt2 = cipher.decrypt(memoryview(bytearray(ct)))
+ self.assertEqual(pt, pt2)
+
+
+class TestVectorsWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings, skip_slow_tests):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._skip_slow_tests = skip_slow_tests
+ self._id = "None"
+
+ def load_tests(self, filename):
+
+ def filter_rsa(group):
+ return RSA.import_key(group['privateKeyPem'])
+
+ def filter_sha(group):
+ if group['sha'] == "SHA-1":
+ return SHA1
+ elif group['sha'] == "SHA-224":
+ return SHA224
+ elif group['sha'] == "SHA-256":
+ return SHA256
+ elif group['sha'] == "SHA-384":
+ return SHA384
+ elif group['sha'] == "SHA-512":
+ return SHA512
+ else:
+ raise ValueError("Unknown sha " + group['sha'])
+
+ def filter_mgf(group):
+ if group['mgfSha'] == "SHA-1":
+ return lambda x, y: MGF1(x, y, SHA1)
+ elif group['mgfSha'] == "SHA-224":
+ return lambda x, y: MGF1(x, y, SHA224)
+ elif group['mgfSha'] == "SHA-256":
+ return lambda x, y: MGF1(x, y, SHA256)
+ elif group['mgfSha'] == "SHA-384":
+ return lambda x, y: MGF1(x, y, SHA384)
+ elif group['mgfSha'] == "SHA-512":
+ return lambda x, y: MGF1(x, y, SHA512)
+ else:
+ raise ValueError("Unknown mgf/sha " + group['mgfSha'])
+
+ def filter_algo(group):
+ return "%s with MGF1/%s" % (group['sha'], group['mgfSha'])
+
+ result = load_test_vectors_wycheproof(("Cipher", "wycheproof"),
+ filename,
+ "Wycheproof PKCS#1 OAEP (%s)" % filename,
+ group_tag={'rsa_key': filter_rsa,
+ 'hash_mod': filter_sha,
+ 'mgf': filter_mgf,
+ 'algo': filter_algo}
+ )
+ return result
+
+ def setUp(self):
+ self.tv = []
+ self.tv.extend(self.load_tests("rsa_oaep_2048_sha1_mgf1sha1_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_2048_sha224_mgf1sha1_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_2048_sha224_mgf1sha224_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_2048_sha256_mgf1sha1_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_2048_sha256_mgf1sha256_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_2048_sha384_mgf1sha1_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_2048_sha384_mgf1sha384_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_2048_sha512_mgf1sha1_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_2048_sha512_mgf1sha512_test.json"))
+ if not self._skip_slow_tests:
+ self.tv.extend(self.load_tests("rsa_oaep_3072_sha256_mgf1sha1_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_3072_sha256_mgf1sha256_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_3072_sha512_mgf1sha1_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_3072_sha512_mgf1sha512_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_4096_sha256_mgf1sha1_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_4096_sha256_mgf1sha256_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_4096_sha512_mgf1sha1_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_4096_sha512_mgf1sha512_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_4096_sha512_mgf1sha512_test.json"))
+ self.tv.extend(self.load_tests("rsa_oaep_misc_test.json"))
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_decrypt(self, tv):
+ self._id = "Wycheproof Decrypt %s Test #%s" % (tv.algo, tv.id)
+
+ cipher = PKCS.new(tv.rsa_key, hashAlgo=tv.hash_mod, mgfunc=tv.mgf, label=tv.label)
+ try:
+ pt = cipher.decrypt(tv.ct)
+ except ValueError:
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.assertEqual(pt, tv.msg)
+ self.warn(tv)
+
+ def runTest(self):
+
+ for tv in self.tv:
+ self.test_decrypt(tv)
+
+
+def get_tests(config={}):
+ skip_slow_tests = not config.get('slow_tests')
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ tests = []
+ tests += list_test_cases(PKCS1_OAEP_Tests)
+ tests += [TestVectorsWycheproof(wycheproof_warnings, skip_slow_tests)]
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/__init__.py
new file mode 100644
index 00000000..98fcab20
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/__init__.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/__init__.py: Self-test for hash modules
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test for hash modules"""
+
+__revision__ = "$Id$"
+
+def get_tests(config={}):
+ tests = []
+ from Crypto.SelfTest.Hash import test_HMAC; tests += test_HMAC.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_CMAC; tests += test_CMAC.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_MD2; tests += test_MD2.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_MD4; tests += test_MD4.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_MD5; tests += test_MD5.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_RIPEMD160; tests += test_RIPEMD160.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_SHA1; tests += test_SHA1.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_SHA224; tests += test_SHA224.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_SHA256; tests += test_SHA256.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_SHA384; tests += test_SHA384.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_SHA512; tests += test_SHA512.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_SHA3_224; tests += test_SHA3_224.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_SHA3_256; tests += test_SHA3_256.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_SHA3_384; tests += test_SHA3_384.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_SHA3_512; tests += test_SHA3_512.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_keccak; tests += test_keccak.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_SHAKE; tests += test_SHAKE.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_BLAKE2; tests += test_BLAKE2.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_Poly1305; tests += test_Poly1305.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_cSHAKE; tests += test_cSHAKE.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_KMAC; tests += test_KMAC.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_TupleHash; tests += test_TupleHash.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_KangarooTwelve; tests += test_KangarooTwelve.get_tests(config=config)
+ from Crypto.SelfTest.Hash import test_TurboSHAKE; tests += test_TurboSHAKE.get_tests(config=config)
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/common.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/common.py
new file mode 100644
index 00000000..15786671
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/common.py
@@ -0,0 +1,290 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/common.py: Common code for Crypto.SelfTest.Hash
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-testing for PyCrypto hash modules"""
+
+import re
+import sys
+import unittest
+import binascii
+import Crypto.Hash
+from binascii import hexlify, unhexlify
+from Crypto.Util.py3compat import b, tobytes
+from Crypto.Util.strxor import strxor_c
+
+def t2b(hex_string):
+ shorter = re.sub(br'\s+', b'', tobytes(hex_string))
+ return unhexlify(shorter)
+
+
+class HashDigestSizeSelfTest(unittest.TestCase):
+
+ def __init__(self, hashmod, description, expected, extra_params):
+ unittest.TestCase.__init__(self)
+ self.hashmod = hashmod
+ self.expected = expected
+ self.description = description
+ self.extra_params = extra_params
+
+ def shortDescription(self):
+ return self.description
+
+ def runTest(self):
+ if "truncate" not in self.extra_params:
+ self.assertTrue(hasattr(self.hashmod, "digest_size"))
+ self.assertEqual(self.hashmod.digest_size, self.expected)
+ h = self.hashmod.new(**self.extra_params)
+ self.assertTrue(hasattr(h, "digest_size"))
+ self.assertEqual(h.digest_size, self.expected)
+
+
+class HashSelfTest(unittest.TestCase):
+
+ def __init__(self, hashmod, description, expected, input, extra_params):
+ unittest.TestCase.__init__(self)
+ self.hashmod = hashmod
+ self.expected = expected.lower()
+ self.input = input
+ self.description = description
+ self.extra_params = extra_params
+
+ def shortDescription(self):
+ return self.description
+
+ def runTest(self):
+ h = self.hashmod.new(**self.extra_params)
+ h.update(self.input)
+
+ out1 = binascii.b2a_hex(h.digest())
+ out2 = h.hexdigest()
+
+ h = self.hashmod.new(self.input, **self.extra_params)
+
+ out3 = h.hexdigest()
+ out4 = binascii.b2a_hex(h.digest())
+
+ # PY3K: hexdigest() should return str(), and digest() bytes
+ self.assertEqual(self.expected, out1) # h = .new(); h.update(data); h.digest()
+ if sys.version_info[0] == 2:
+ self.assertEqual(self.expected, out2) # h = .new(); h.update(data); h.hexdigest()
+ self.assertEqual(self.expected, out3) # h = .new(data); h.hexdigest()
+ else:
+ self.assertEqual(self.expected.decode(), out2) # h = .new(); h.update(data); h.hexdigest()
+ self.assertEqual(self.expected.decode(), out3) # h = .new(data); h.hexdigest()
+ self.assertEqual(self.expected, out4) # h = .new(data); h.digest()
+
+ # Verify that the .new() method produces a fresh hash object, except
+ # for MD5 and SHA1, which are hashlib objects. (But test any .new()
+ # method that does exist.)
+ if self.hashmod.__name__ not in ('Crypto.Hash.MD5', 'Crypto.Hash.SHA1') or hasattr(h, 'new'):
+ h2 = h.new()
+ h2.update(self.input)
+ out5 = binascii.b2a_hex(h2.digest())
+ self.assertEqual(self.expected, out5)
+
+
+class HashTestOID(unittest.TestCase):
+ def __init__(self, hashmod, oid, extra_params):
+ unittest.TestCase.__init__(self)
+ self.hashmod = hashmod
+ self.oid = oid
+ self.extra_params = extra_params
+
+ def runTest(self):
+ h = self.hashmod.new(**self.extra_params)
+ self.assertEqual(h.oid, self.oid)
+
+
+class ByteArrayTest(unittest.TestCase):
+
+ def __init__(self, module, extra_params):
+ unittest.TestCase.__init__(self)
+ self.module = module
+ self.extra_params = extra_params
+
+ def runTest(self):
+ data = b("\x00\x01\x02")
+
+ # Data can be a bytearray (during initialization)
+ ba = bytearray(data)
+
+ h1 = self.module.new(data, **self.extra_params)
+ h2 = self.module.new(ba, **self.extra_params)
+ ba[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a bytearray (during operation)
+ ba = bytearray(data)
+
+ h1 = self.module.new(**self.extra_params)
+ h2 = self.module.new(**self.extra_params)
+
+ h1.update(data)
+ h2.update(ba)
+
+ ba[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+
+class MemoryViewTest(unittest.TestCase):
+
+ def __init__(self, module, extra_params):
+ unittest.TestCase.__init__(self)
+ self.module = module
+ self.extra_params = extra_params
+
+ def runTest(self):
+
+ data = b"\x00\x01\x02"
+
+ def get_mv_ro(data):
+ return memoryview(data)
+
+ def get_mv_rw(data):
+ return memoryview(bytearray(data))
+
+ for get_mv in get_mv_ro, get_mv_rw:
+
+ # Data can be a memoryview (during initialization)
+ mv = get_mv(data)
+
+ h1 = self.module.new(data, **self.extra_params)
+ h2 = self.module.new(mv, **self.extra_params)
+ if not mv.readonly:
+ mv[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a memoryview (during operation)
+ mv = get_mv(data)
+
+ h1 = self.module.new(**self.extra_params)
+ h2 = self.module.new(**self.extra_params)
+ h1.update(data)
+ h2.update(mv)
+ if not mv.readonly:
+ mv[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+
+class MACSelfTest(unittest.TestCase):
+
+ def __init__(self, module, description, result, data, key, params):
+ unittest.TestCase.__init__(self)
+ self.module = module
+ self.result = t2b(result)
+ self.data = t2b(data)
+ self.key = t2b(key)
+ self.params = params
+ self.description = description
+
+ def shortDescription(self):
+ return self.description
+
+ def runTest(self):
+
+ result_hex = hexlify(self.result)
+
+ # Verify result
+ h = self.module.new(self.key, **self.params)
+ h.update(self.data)
+ self.assertEqual(self.result, h.digest())
+ self.assertEqual(hexlify(self.result).decode('ascii'), h.hexdigest())
+
+ # Verify that correct MAC does not raise any exception
+ h.verify(self.result)
+ h.hexverify(result_hex)
+
+ # Verify that incorrect MAC does raise ValueError exception
+ wrong_mac = strxor_c(self.result, 255)
+ self.assertRaises(ValueError, h.verify, wrong_mac)
+ self.assertRaises(ValueError, h.hexverify, "4556")
+
+ # Verify again, with data passed to new()
+ h = self.module.new(self.key, self.data, **self.params)
+ self.assertEqual(self.result, h.digest())
+ self.assertEqual(hexlify(self.result).decode('ascii'), h.hexdigest())
+
+ # Test .copy()
+ try:
+ h = self.module.new(self.key, self.data, **self.params)
+ h2 = h.copy()
+ h3 = h.copy()
+
+ # Verify that changing the copy does not change the original
+ h2.update(b"bla")
+ self.assertEqual(h3.digest(), self.result)
+
+ # Verify that both can reach the same state
+ h.update(b"bla")
+ self.assertEqual(h.digest(), h2.digest())
+ except NotImplementedError:
+ pass
+
+ # PY3K: Check that hexdigest() returns str and digest() returns bytes
+ self.assertTrue(isinstance(h.digest(), type(b"")))
+ self.assertTrue(isinstance(h.hexdigest(), type("")))
+
+ # PY3K: Check that .hexverify() accepts bytes or str
+ h.hexverify(h.hexdigest())
+ h.hexverify(h.hexdigest().encode('ascii'))
+
+
+def make_hash_tests(module, module_name, test_data, digest_size, oid=None,
+ extra_params={}):
+ tests = []
+ for i in range(len(test_data)):
+ row = test_data[i]
+ (expected, input) = map(tobytes,row[0:2])
+ if len(row) < 3:
+ description = repr(input)
+ else:
+ description = row[2]
+ name = "%s #%d: %s" % (module_name, i+1, description)
+ tests.append(HashSelfTest(module, name, expected, input, extra_params))
+
+ name = "%s #%d: digest_size" % (module_name, len(test_data) + 1)
+ tests.append(HashDigestSizeSelfTest(module, name, digest_size, extra_params))
+
+ if oid is not None:
+ tests.append(HashTestOID(module, oid, extra_params))
+
+ tests.append(ByteArrayTest(module, extra_params))
+
+ tests.append(MemoryViewTest(module, extra_params))
+
+ return tests
+
+
+def make_mac_tests(module, module_name, test_data):
+ tests = []
+ for i, row in enumerate(test_data):
+ if len(row) == 4:
+ (key, data, results, description, params) = list(row) + [ {} ]
+ else:
+ (key, data, results, description, params) = row
+ name = "%s #%d: %s" % (module_name, i+1, description)
+ tests.append(MACSelfTest(module, name, results, data, key, params))
+ return tests
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_BLAKE2.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_BLAKE2.py
new file mode 100644
index 00000000..f953eabd
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_BLAKE2.py
@@ -0,0 +1,482 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 os
+import re
+import unittest
+import warnings
+from binascii import unhexlify, hexlify
+
+from Crypto.Util.py3compat import tobytes
+from Crypto.Util.strxor import strxor_c
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Hash import BLAKE2b, BLAKE2s
+
+
+class Blake2Test(unittest.TestCase):
+
+ def test_new_positive(self):
+
+ h = self.BLAKE2.new(digest_bits=self.max_bits)
+ for new_func in self.BLAKE2.new, h.new:
+
+ for dbits in range(8, self.max_bits + 1, 8):
+ hobj = new_func(digest_bits=dbits)
+ self.assertEqual(hobj.digest_size, dbits // 8)
+
+ for dbytes in range(1, self.max_bytes + 1):
+ hobj = new_func(digest_bytes=dbytes)
+ self.assertEqual(hobj.digest_size, dbytes)
+
+ digest1 = new_func(data=b"\x90", digest_bytes=self.max_bytes).digest()
+ digest2 = new_func(digest_bytes=self.max_bytes).update(b"\x90").digest()
+ self.assertEqual(digest1, digest2)
+
+ new_func(data=b"A", key=b"5", digest_bytes=self.max_bytes)
+
+ hobj = h.new()
+ self.assertEqual(hobj.digest_size, self.max_bytes)
+
+ def test_new_negative(self):
+
+ h = self.BLAKE2.new(digest_bits=self.max_bits)
+ for new_func in self.BLAKE2.new, h.new:
+ self.assertRaises(TypeError, new_func,
+ digest_bytes=self.max_bytes,
+ digest_bits=self.max_bits)
+ self.assertRaises(ValueError, new_func, digest_bytes=0)
+ self.assertRaises(ValueError, new_func,
+ digest_bytes=self.max_bytes + 1)
+ self.assertRaises(ValueError, new_func, digest_bits=7)
+ self.assertRaises(ValueError, new_func, digest_bits=15)
+ self.assertRaises(ValueError, new_func,
+ digest_bits=self.max_bits + 1)
+ self.assertRaises(TypeError, new_func,
+ digest_bytes=self.max_bytes,
+ key=u"string")
+ self.assertRaises(TypeError, new_func,
+ digest_bytes=self.max_bytes,
+ data=u"string")
+
+ def test_default_digest_size(self):
+ digest = self.BLAKE2.new(data=b'abc').digest()
+ self.assertEqual(len(digest), self.max_bytes)
+
+ def test_update(self):
+ pieces = [b"\x0A" * 200, b"\x14" * 300]
+ h = self.BLAKE2.new(digest_bytes=self.max_bytes)
+ h.update(pieces[0]).update(pieces[1])
+ digest = h.digest()
+ h = self.BLAKE2.new(digest_bytes=self.max_bytes)
+ h.update(pieces[0] + pieces[1])
+ self.assertEqual(h.digest(), digest)
+
+ def test_update_negative(self):
+ h = self.BLAKE2.new(digest_bytes=self.max_bytes)
+ self.assertRaises(TypeError, h.update, u"string")
+
+ def test_digest(self):
+ h = self.BLAKE2.new(digest_bytes=self.max_bytes)
+ digest = h.digest()
+
+ # hexdigest does not change the state
+ self.assertEqual(h.digest(), digest)
+ # digest returns a byte string
+ self.assertTrue(isinstance(digest, type(b"digest")))
+
+ def test_update_after_digest(self):
+ msg = b"rrrrttt"
+
+ # Normally, update() cannot be done after digest()
+ h = self.BLAKE2.new(digest_bits=256, data=msg[:4])
+ dig1 = h.digest()
+ self.assertRaises(TypeError, h.update, msg[4:])
+ dig2 = self.BLAKE2.new(digest_bits=256, data=msg).digest()
+
+ # With the proper flag, it is allowed
+ h = self.BLAKE2.new(digest_bits=256, data=msg[:4], update_after_digest=True)
+ self.assertEqual(h.digest(), dig1)
+ # ... and the subsequent digest applies to the entire message
+ # up to that point
+ h.update(msg[4:])
+ self.assertEqual(h.digest(), dig2)
+
+ def test_hex_digest(self):
+ mac = self.BLAKE2.new(digest_bits=self.max_bits)
+ digest = mac.digest()
+ hexdigest = mac.hexdigest()
+
+ # hexdigest is equivalent to digest
+ self.assertEqual(hexlify(digest), tobytes(hexdigest))
+ # hexdigest does not change the state
+ self.assertEqual(mac.hexdigest(), hexdigest)
+ # hexdigest returns a string
+ self.assertTrue(isinstance(hexdigest, type("digest")))
+
+ def test_verify(self):
+ h = self.BLAKE2.new(digest_bytes=self.max_bytes, key=b"4")
+ mac = h.digest()
+ h.verify(mac)
+ wrong_mac = strxor_c(mac, 255)
+ self.assertRaises(ValueError, h.verify, wrong_mac)
+
+ def test_hexverify(self):
+ h = self.BLAKE2.new(digest_bytes=self.max_bytes, key=b"4")
+ mac = h.hexdigest()
+ h.hexverify(mac)
+ self.assertRaises(ValueError, h.hexverify, "4556")
+
+ def test_oid(self):
+
+ prefix = "1.3.6.1.4.1.1722.12.2." + self.oid_variant + "."
+
+ for digest_bits in self.digest_bits_oid:
+ h = self.BLAKE2.new(digest_bits=digest_bits)
+ self.assertEqual(h.oid, prefix + str(digest_bits // 8))
+
+ h = self.BLAKE2.new(digest_bits=digest_bits, key=b"secret")
+ self.assertRaises(AttributeError, lambda: h.oid)
+
+ for digest_bits in (8, self.max_bits):
+ if digest_bits in self.digest_bits_oid:
+ continue
+ self.assertRaises(AttributeError, lambda: h.oid)
+
+ def test_bytearray(self):
+
+ key = b'0' * 16
+ data = b"\x00\x01\x02"
+
+ # Data and key can be a bytearray (during initialization)
+ key_ba = bytearray(key)
+ data_ba = bytearray(data)
+
+ h1 = self.BLAKE2.new(data=data, key=key)
+ h2 = self.BLAKE2.new(data=data_ba, key=key_ba)
+ key_ba[:1] = b'\xFF'
+ data_ba[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a bytearray (during operation)
+ data_ba = bytearray(data)
+
+ h1 = self.BLAKE2.new()
+ h2 = self.BLAKE2.new()
+ h1.update(data)
+ h2.update(data_ba)
+ data_ba[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+ def test_memoryview(self):
+
+ key = b'0' * 16
+ data = b"\x00\x01\x02"
+
+ def get_mv_ro(data):
+ return memoryview(data)
+
+ def get_mv_rw(data):
+ return memoryview(bytearray(data))
+
+ for get_mv in (get_mv_ro, get_mv_rw):
+
+ # Data and key can be a memoryview (during initialization)
+ key_mv = get_mv(key)
+ data_mv = get_mv(data)
+
+ h1 = self.BLAKE2.new(data=data, key=key)
+ h2 = self.BLAKE2.new(data=data_mv, key=key_mv)
+ if not data_mv.readonly:
+ data_mv[:1] = b'\xFF'
+ key_mv[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a memoryview (during operation)
+ data_mv = get_mv(data)
+
+ h1 = self.BLAKE2.new()
+ h2 = self.BLAKE2.new()
+ h1.update(data)
+ h2.update(data_mv)
+ if not data_mv.readonly:
+ data_mv[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+
+class Blake2bTest(Blake2Test):
+ #: Module
+ BLAKE2 = BLAKE2b
+ #: Max output size (in bits)
+ max_bits = 512
+ #: Max output size (in bytes)
+ max_bytes = 64
+ #: Bit size of the digests for which an ASN OID exists
+ digest_bits_oid = (160, 256, 384, 512)
+ # http://tools.ietf.org/html/draft-saarinen-blake2-02
+ oid_variant = "1"
+
+
+class Blake2sTest(Blake2Test):
+ #: Module
+ BLAKE2 = BLAKE2s
+ #: Max output size (in bits)
+ max_bits = 256
+ #: Max output size (in bytes)
+ max_bytes = 32
+ #: Bit size of the digests for which an ASN OID exists
+ digest_bits_oid = (128, 160, 224, 256)
+ # http://tools.ietf.org/html/draft-saarinen-blake2-02
+ oid_variant = "2"
+
+
+class Blake2OfficialTestVector(unittest.TestCase):
+
+ def _load_tests(self, test_vector_file):
+ expected = "in"
+ test_vectors = []
+ with open(test_vector_file, "rt") as test_vector_fd:
+ for line_number, line in enumerate(test_vector_fd):
+
+ if line.strip() == "" or line.startswith("#"):
+ continue
+
+ res = re.match("%s:\t([0-9A-Fa-f]*)" % expected, line)
+ if not res:
+ raise ValueError("Incorrect test vector format (line %d)"
+ % line_number)
+
+ if res.group(1):
+ bin_value = unhexlify(tobytes(res.group(1)))
+ else:
+ bin_value = b""
+ if expected == "in":
+ input_data = bin_value
+ expected = "key"
+ elif expected == "key":
+ key = bin_value
+ expected = "hash"
+ else:
+ result = bin_value
+ expected = "in"
+ test_vectors.append((input_data, key, result))
+ return test_vectors
+
+ def setUp(self):
+
+ dir_comps = ("Hash", self.name)
+ file_name = self.name.lower() + "-test.txt"
+ self.description = "%s tests" % self.name
+
+ try:
+ import pycryptodome_test_vectors # type: ignore
+ except ImportError:
+ warnings.warn("Warning: skipping extended tests for %s" % self.name,
+ UserWarning)
+ self.test_vectors = []
+ return
+
+ init_dir = os.path.dirname(pycryptodome_test_vectors.__file__)
+ full_file_name = os.path.join(os.path.join(init_dir, *dir_comps), file_name)
+ self.test_vectors = self._load_tests(full_file_name)
+
+ def runTest(self):
+ for (input_data, key, result) in self.test_vectors:
+ mac = self.BLAKE2.new(key=key, digest_bytes=self.max_bytes)
+ mac.update(input_data)
+ self.assertEqual(mac.digest(), result)
+
+
+class Blake2bOfficialTestVector(Blake2OfficialTestVector):
+ #: Module
+ BLAKE2 = BLAKE2b
+ #: Hash name
+ name = "BLAKE2b"
+ #: Max digest size
+ max_bytes = 64
+
+
+class Blake2sOfficialTestVector(Blake2OfficialTestVector):
+ #: Module
+ BLAKE2 = BLAKE2s
+ #: Hash name
+ name = "BLAKE2s"
+ #: Max digest size
+ max_bytes = 32
+
+
+class Blake2TestVector1(unittest.TestCase):
+
+ def _load_tests(self, test_vector_file):
+ test_vectors = []
+ with open(test_vector_file, "rt") as test_vector_fd:
+ for line_number, line in enumerate(test_vector_fd):
+ if line.strip() == "" or line.startswith("#"):
+ continue
+ res = re.match("digest: ([0-9A-Fa-f]*)", line)
+ if not res:
+ raise ValueError("Incorrect test vector format (line %d)"
+ % line_number)
+
+ test_vectors.append(unhexlify(tobytes(res.group(1))))
+ return test_vectors
+
+ def setUp(self):
+ dir_comps = ("Hash", self.name)
+ file_name = "tv1.txt"
+ self.description = "%s tests" % self.name
+
+ try:
+ import pycryptodome_test_vectors
+ except ImportError:
+ warnings.warn("Warning: skipping extended tests for %s" % self.name,
+ UserWarning)
+ self.test_vectors = []
+ return
+
+ init_dir = os.path.dirname(pycryptodome_test_vectors.__file__)
+ full_file_name = os.path.join(os.path.join(init_dir, *dir_comps), file_name)
+ self.test_vectors = self._load_tests(full_file_name)
+
+ def runTest(self):
+
+ for tv in self.test_vectors:
+ digest_bytes = len(tv)
+ next_data = b""
+ for _ in range(100):
+ h = self.BLAKE2.new(digest_bytes=digest_bytes)
+ h.update(next_data)
+ next_data = h.digest() + next_data
+ self.assertEqual(h.digest(), tv)
+
+
+class Blake2bTestVector1(Blake2TestVector1):
+ #: Module
+ BLAKE2 = BLAKE2b
+ #: Hash name
+ name = "BLAKE2b"
+
+
+class Blake2sTestVector1(Blake2TestVector1):
+ #: Module
+ BLAKE2 = BLAKE2s
+ #: Hash name
+ name = "BLAKE2s"
+
+
+class Blake2TestVector2(unittest.TestCase):
+
+ def _load_tests(self, test_vector_file):
+ test_vectors = []
+ with open(test_vector_file, "rt") as test_vector_fd:
+ for line_number, line in enumerate(test_vector_fd):
+ if line.strip() == "" or line.startswith("#"):
+ continue
+ res = re.match(r"digest\(([0-9]+)\): ([0-9A-Fa-f]*)", line)
+ if not res:
+ raise ValueError("Incorrect test vector format (line %d)"
+ % line_number)
+ key_size = int(res.group(1))
+ result = unhexlify(tobytes(res.group(2)))
+ test_vectors.append((key_size, result))
+ return test_vectors
+
+ def setUp(self):
+ dir_comps = ("Hash", self.name)
+ file_name = "tv2.txt"
+ self.description = "%s tests" % self.name
+
+ try:
+ import pycryptodome_test_vectors # type: ignore
+ except ImportError:
+ warnings.warn("Warning: skipping extended tests for %s" % self.name,
+ UserWarning)
+ self.test_vectors = []
+ return
+
+ init_dir = os.path.dirname(pycryptodome_test_vectors.__file__)
+ full_file_name = os.path.join(os.path.join(init_dir, *dir_comps), file_name)
+ self.test_vectors = self._load_tests(full_file_name)
+
+ def runTest(self):
+
+ for key_size, result in self.test_vectors:
+ next_data = b""
+ for _ in range(100):
+ h = self.BLAKE2.new(digest_bytes=self.max_bytes,
+ key=b"A" * key_size)
+ h.update(next_data)
+ next_data = h.digest() + next_data
+ self.assertEqual(h.digest(), result)
+
+
+class Blake2bTestVector2(Blake2TestVector1):
+ #: Module
+ BLAKE2 = BLAKE2b
+ #: Hash name
+ name = "BLAKE2b"
+ #: Max digest size in bytes
+ max_bytes = 64
+
+
+class Blake2sTestVector2(Blake2TestVector1):
+ #: Module
+ BLAKE2 = BLAKE2s
+ #: Hash name
+ name = "BLAKE2s"
+ #: Max digest size in bytes
+ max_bytes = 32
+
+
+def get_tests(config={}):
+ tests = []
+
+ tests += list_test_cases(Blake2bTest)
+ tests.append(Blake2bOfficialTestVector())
+ tests.append(Blake2bTestVector1())
+ tests.append(Blake2bTestVector2())
+
+ tests += list_test_cases(Blake2sTest)
+ tests.append(Blake2sOfficialTestVector())
+ tests.append(Blake2sTestVector1())
+ tests.append(Blake2sTestVector2())
+
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_CMAC.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_CMAC.py
new file mode 100644
index 00000000..f4763f21
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_CMAC.py
@@ -0,0 +1,448 @@
+#
+# SelfTest/Hash/CMAC.py: Self-test for the CMAC module
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.CMAC"""
+
+import json
+import unittest
+from binascii import unhexlify
+
+from Crypto.Util.py3compat import tobytes
+
+from Crypto.Hash import CMAC
+from Crypto.Cipher import AES, DES3
+from Crypto.Hash import SHAKE128
+
+from Crypto.Util.strxor import strxor
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors_wycheproof
+
+# This is a list of (key, data, result, description, module) tuples.
+test_data = [
+
+ ## Test vectors from RFC 4493 ##
+ ## The are also in NIST SP 800 38B D.2 ##
+ ( '2b7e151628aed2a6abf7158809cf4f3c',
+ '',
+ 'bb1d6929e95937287fa37d129b756746',
+ 'RFC 4493 #1',
+ AES
+ ),
+
+ ( '2b7e151628aed2a6abf7158809cf4f3c',
+ '6bc1bee22e409f96e93d7e117393172a',
+ '070a16b46b4d4144f79bdd9dd04a287c',
+ 'RFC 4493 #2',
+ AES
+ ),
+
+ ( '2b7e151628aed2a6abf7158809cf4f3c',
+ '6bc1bee22e409f96e93d7e117393172a'+
+ 'ae2d8a571e03ac9c9eb76fac45af8e51'+
+ '30c81c46a35ce411',
+ 'dfa66747de9ae63030ca32611497c827',
+ 'RFC 4493 #3',
+ AES
+ ),
+
+ ( '2b7e151628aed2a6abf7158809cf4f3c',
+ '6bc1bee22e409f96e93d7e117393172a'+
+ 'ae2d8a571e03ac9c9eb76fac45af8e51'+
+ '30c81c46a35ce411e5fbc1191a0a52ef'+
+ 'f69f2445df4f9b17ad2b417be66c3710',
+ '51f0bebf7e3b9d92fc49741779363cfe',
+ 'RFC 4493 #4',
+ AES
+ ),
+
+ ## The rest of Appendix D of NIST SP 800 38B
+ ## was not totally correct.
+ ## Values in Examples 14, 15, 18, and 19 were wrong.
+ ## The updated test values are published in:
+ ## http://csrc.nist.gov/publications/nistpubs/800-38B/Updated_CMAC_Examples.pdf
+
+ ( '8e73b0f7da0e6452c810f32b809079e5'+
+ '62f8ead2522c6b7b',
+ '',
+ 'd17ddf46adaacde531cac483de7a9367',
+ 'NIST SP 800 38B D.2 Example 5',
+ AES
+ ),
+
+ ( '8e73b0f7da0e6452c810f32b809079e5'+
+ '62f8ead2522c6b7b',
+ '6bc1bee22e409f96e93d7e117393172a',
+ '9e99a7bf31e710900662f65e617c5184',
+ 'NIST SP 800 38B D.2 Example 6',
+ AES
+ ),
+
+ ( '8e73b0f7da0e6452c810f32b809079e5'+
+ '62f8ead2522c6b7b',
+ '6bc1bee22e409f96e93d7e117393172a'+
+ 'ae2d8a571e03ac9c9eb76fac45af8e51'+
+ '30c81c46a35ce411',
+ '8a1de5be2eb31aad089a82e6ee908b0e',
+ 'NIST SP 800 38B D.2 Example 7',
+ AES
+ ),
+
+ ( '8e73b0f7da0e6452c810f32b809079e5'+
+ '62f8ead2522c6b7b',
+ '6bc1bee22e409f96e93d7e117393172a'+
+ 'ae2d8a571e03ac9c9eb76fac45af8e51'+
+ '30c81c46a35ce411e5fbc1191a0a52ef'+
+ 'f69f2445df4f9b17ad2b417be66c3710',
+ 'a1d5df0eed790f794d77589659f39a11',
+ 'NIST SP 800 38B D.2 Example 8',
+ AES
+ ),
+
+ ( '603deb1015ca71be2b73aef0857d7781'+
+ '1f352c073b6108d72d9810a30914dff4',
+ '',
+ '028962f61b7bf89efc6b551f4667d983',
+ 'NIST SP 800 38B D.3 Example 9',
+ AES
+ ),
+
+ ( '603deb1015ca71be2b73aef0857d7781'+
+ '1f352c073b6108d72d9810a30914dff4',
+ '6bc1bee22e409f96e93d7e117393172a',
+ '28a7023f452e8f82bd4bf28d8c37c35c',
+ 'NIST SP 800 38B D.3 Example 10',
+ AES
+ ),
+
+ ( '603deb1015ca71be2b73aef0857d7781'+
+ '1f352c073b6108d72d9810a30914dff4',
+ '6bc1bee22e409f96e93d7e117393172a'+
+ 'ae2d8a571e03ac9c9eb76fac45af8e51'+
+ '30c81c46a35ce411',
+ 'aaf3d8f1de5640c232f5b169b9c911e6',
+ 'NIST SP 800 38B D.3 Example 11',
+ AES
+ ),
+
+ ( '603deb1015ca71be2b73aef0857d7781'+
+ '1f352c073b6108d72d9810a30914dff4',
+ '6bc1bee22e409f96e93d7e117393172a'+
+ 'ae2d8a571e03ac9c9eb76fac45af8e51'+
+ '30c81c46a35ce411e5fbc1191a0a52ef'+
+ 'f69f2445df4f9b17ad2b417be66c3710',
+ 'e1992190549f6ed5696a2c056c315410',
+ 'NIST SP 800 38B D.3 Example 12',
+ AES
+ ),
+
+ ( '8aa83bf8cbda1062'+
+ '0bc1bf19fbb6cd58'+
+ 'bc313d4a371ca8b5',
+ '',
+ 'b7a688e122ffaf95',
+ 'NIST SP 800 38B D.4 Example 13',
+ DES3
+ ),
+
+ ( '8aa83bf8cbda1062'+
+ '0bc1bf19fbb6cd58'+
+ 'bc313d4a371ca8b5',
+ '6bc1bee22e409f96',
+ '8e8f293136283797',
+ 'NIST SP 800 38B D.4 Example 14',
+ DES3
+ ),
+
+ ( '8aa83bf8cbda1062'+
+ '0bc1bf19fbb6cd58'+
+ 'bc313d4a371ca8b5',
+ '6bc1bee22e409f96'+
+ 'e93d7e117393172a'+
+ 'ae2d8a57',
+ '743ddbe0ce2dc2ed',
+ 'NIST SP 800 38B D.4 Example 15',
+ DES3
+ ),
+
+ ( '8aa83bf8cbda1062'+
+ '0bc1bf19fbb6cd58'+
+ 'bc313d4a371ca8b5',
+ '6bc1bee22e409f96'+
+ 'e93d7e117393172a'+
+ 'ae2d8a571e03ac9c'+
+ '9eb76fac45af8e51',
+ '33e6b1092400eae5',
+ 'NIST SP 800 38B D.4 Example 16',
+ DES3
+ ),
+
+ ( '4cf15134a2850dd5'+
+ '8a3d10ba80570d38',
+ '',
+ 'bd2ebf9a3ba00361',
+ 'NIST SP 800 38B D.7 Example 17',
+ DES3
+ ),
+
+ ( '4cf15134a2850dd5'+
+ '8a3d10ba80570d38',
+ '6bc1bee22e409f96',
+ '4ff2ab813c53ce83',
+ 'NIST SP 800 38B D.7 Example 18',
+ DES3
+ ),
+
+ ( '4cf15134a2850dd5'+
+ '8a3d10ba80570d38',
+ '6bc1bee22e409f96'+
+ 'e93d7e117393172a'+
+ 'ae2d8a57',
+ '62dd1b471902bd4e',
+ 'NIST SP 800 38B D.7 Example 19',
+ DES3
+ ),
+
+ ( '4cf15134a2850dd5'+
+ '8a3d10ba80570d38',
+ '6bc1bee22e409f96'+
+ 'e93d7e117393172a'+
+ 'ae2d8a571e03ac9c'+
+ '9eb76fac45af8e51',
+ '31b1e431dabc4eb8',
+ 'NIST SP 800 38B D.7 Example 20',
+ DES3
+ ),
+
+]
+
+
+def get_tag_random(tag, length):
+ return SHAKE128.new(data=tobytes(tag)).read(length)
+
+
+class TestCMAC(unittest.TestCase):
+
+ def test_internal_caching(self):
+ """Verify that internal caching is implemented correctly"""
+
+ data_to_mac = get_tag_random("data_to_mac", 128)
+ key = get_tag_random("key", 16)
+ ref_mac = CMAC.new(key, msg=data_to_mac, ciphermod=AES).digest()
+
+ # Break up in chunks of different length
+ # The result must always be the same
+ for chunk_length in 1, 2, 3, 7, 10, 13, 16, 40, 80, 128:
+
+ chunks = [data_to_mac[i:i+chunk_length] for i in
+ range(0, len(data_to_mac), chunk_length)]
+
+ mac = CMAC.new(key, ciphermod=AES)
+ for chunk in chunks:
+ mac.update(chunk)
+ self.assertEqual(ref_mac, mac.digest())
+
+ def test_update_after_digest(self):
+ msg = b"rrrrttt"
+ key = b"4" * 16
+
+ # Normally, update() cannot be done after digest()
+ h = CMAC.new(key, msg[:4], ciphermod=AES)
+ dig1 = h.digest()
+ self.assertRaises(TypeError, h.update, msg[4:])
+ dig2 = CMAC.new(key, msg, ciphermod=AES).digest()
+
+ # With the proper flag, it is allowed
+ h2 = CMAC.new(key, msg[:4], ciphermod=AES, update_after_digest=True)
+ self.assertEqual(h2.digest(), dig1)
+ # ... and the subsequent digest applies to the entire message
+ # up to that point
+ h2.update(msg[4:])
+ self.assertEqual(h2.digest(), dig2)
+
+
+class ByteArrayTests(unittest.TestCase):
+
+ def runTest(self):
+
+ key = b"0" * 16
+ data = b"\x00\x01\x02"
+
+ # Data and key can be a bytearray (during initialization)
+ key_ba = bytearray(key)
+ data_ba = bytearray(data)
+
+ h1 = CMAC.new(key, data, ciphermod=AES)
+ h2 = CMAC.new(key_ba, data_ba, ciphermod=AES)
+ key_ba[:1] = b'\xFF'
+ data_ba[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a bytearray (during operation)
+ key_ba = bytearray(key)
+ data_ba = bytearray(data)
+
+ h1 = CMAC.new(key, ciphermod=AES)
+ h2 = CMAC.new(key, ciphermod=AES)
+ h1.update(data)
+ h2.update(data_ba)
+ data_ba[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+
+class MemoryViewTests(unittest.TestCase):
+
+ def runTest(self):
+
+ key = b"0" * 16
+ data = b"\x00\x01\x02"
+
+ def get_mv_ro(data):
+ return memoryview(data)
+
+ def get_mv_rw(data):
+ return memoryview(bytearray(data))
+
+ for get_mv in (get_mv_ro, get_mv_rw):
+
+ # Data and key can be a memoryview (during initialization)
+ key_mv = get_mv(key)
+ data_mv = get_mv(data)
+
+ h1 = CMAC.new(key, data, ciphermod=AES)
+ h2 = CMAC.new(key_mv, data_mv, ciphermod=AES)
+ if not data_mv.readonly:
+ key_mv[:1] = b'\xFF'
+ data_mv[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a memoryview (during operation)
+ data_mv = get_mv(data)
+
+ h1 = CMAC.new(key, ciphermod=AES)
+ h2 = CMAC.new(key, ciphermod=AES)
+ h1.update(data)
+ h2.update(data_mv)
+ if not data_mv.readonly:
+ data_mv[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+
+class TestVectorsWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._id = "None"
+
+ def setUp(self):
+
+ def filter_tag(group):
+ return group['tagSize'] // 8
+
+ self.tv = load_test_vectors_wycheproof(("Hash", "wycheproof"),
+ "aes_cmac_test.json",
+ "Wycheproof CMAC",
+ group_tag={'tag_size': filter_tag})
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_create_mac(self, tv):
+ self._id = "Wycheproof MAC creation Test #" + str(tv.id)
+
+ try:
+ tag = CMAC.new(tv.key, tv.msg, ciphermod=AES, mac_len=tv.tag_size).digest()
+ except ValueError as e:
+ if len(tv.key) not in (16, 24, 32) and "key length" in str(e):
+ return
+ raise e
+ if tv.valid:
+ self.assertEqual(tag, tv.tag)
+ self.warn(tv)
+
+ def test_verify_mac(self, tv):
+ self._id = "Wycheproof MAC verification Test #" + str(tv.id)
+
+ try:
+ mac = CMAC.new(tv.key, tv.msg, ciphermod=AES, mac_len=tv.tag_size)
+ except ValueError as e:
+ if len(tv.key) not in (16, 24, 32) and "key length" in str(e):
+ return
+ raise e
+ try:
+ mac.verify(tv.tag)
+ except ValueError:
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.warn(tv)
+
+ def runTest(self):
+
+ for tv in self.tv:
+ self.test_create_mac(tv)
+ self.test_verify_mac(tv)
+
+
+def get_tests(config={}):
+ global test_data
+ import types
+ from .common import make_mac_tests
+
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ # Add new() parameters to the back of each test vector
+ params_test_data = []
+ for row in test_data:
+ t = list(row)
+ t[4] = dict(ciphermod=t[4])
+ params_test_data.append(t)
+
+ tests = make_mac_tests(CMAC, "CMAC", params_test_data)
+ tests.append(ByteArrayTests())
+ tests.append(list_test_cases(TestCMAC))
+ tests.append(MemoryViewTests())
+ tests += [ TestVectorsWycheproof(wycheproof_warnings) ]
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_HMAC.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_HMAC.py
new file mode 100644
index 00000000..26b7b247
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_HMAC.py
@@ -0,0 +1,548 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/HMAC.py: Self-test for the HMAC module
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.HMAC"""
+
+import unittest
+from binascii import hexlify
+from Crypto.Util.py3compat import tostr, tobytes
+
+from Crypto.Hash import (HMAC, MD5, SHA1, SHA256,
+ SHA224, SHA384, SHA512,
+ RIPEMD160,
+ SHA3_224, SHA3_256, SHA3_384, SHA3_512)
+
+
+hash_modules = dict(MD5=MD5, SHA1=SHA1, SHA256=SHA256,
+ SHA224=SHA224, SHA384=SHA384, SHA512=SHA512,
+ RIPEMD160=RIPEMD160,
+ SHA3_224=SHA3_224, SHA3_256=SHA3_256,
+ SHA3_384=SHA3_384, SHA3_512=SHA3_512)
+
+default_hash = None
+
+def xl(text):
+ return tostr(hexlify(tobytes(text)))
+
+# This is a list of (key, data, results, description) tuples.
+test_data = [
+ ## Test vectors from RFC 2202 ##
+ # Test that the default hashmod is MD5
+ ('0b' * 16,
+ '4869205468657265',
+ dict(default_hash='9294727a3638bb1c13f48ef8158bfc9d'),
+ 'default-is-MD5'),
+
+ # Test case 1 (MD5)
+ ('0b' * 16,
+ '4869205468657265',
+ dict(MD5='9294727a3638bb1c13f48ef8158bfc9d'),
+ 'RFC 2202 #1-MD5 (HMAC-MD5)'),
+
+ # Test case 1 (SHA1)
+ ('0b' * 20,
+ '4869205468657265',
+ dict(SHA1='b617318655057264e28bc0b6fb378c8ef146be00'),
+ 'RFC 2202 #1-SHA1 (HMAC-SHA1)'),
+
+ # Test case 2
+ ('4a656665',
+ '7768617420646f2079612077616e7420666f72206e6f7468696e673f',
+ dict(MD5='750c783e6ab0b503eaa86e310a5db738',
+ SHA1='effcdf6ae5eb2fa2d27416d5f184df9c259a7c79'),
+ 'RFC 2202 #2 (HMAC-MD5/SHA1)'),
+
+ # Test case 3 (MD5)
+ ('aa' * 16,
+ 'dd' * 50,
+ dict(MD5='56be34521d144c88dbb8c733f0e8b3f6'),
+ 'RFC 2202 #3-MD5 (HMAC-MD5)'),
+
+ # Test case 3 (SHA1)
+ ('aa' * 20,
+ 'dd' * 50,
+ dict(SHA1='125d7342b9ac11cd91a39af48aa17b4f63f175d3'),
+ 'RFC 2202 #3-SHA1 (HMAC-SHA1)'),
+
+ # Test case 4
+ ('0102030405060708090a0b0c0d0e0f10111213141516171819',
+ 'cd' * 50,
+ dict(MD5='697eaf0aca3a3aea3a75164746ffaa79',
+ SHA1='4c9007f4026250c6bc8414f9bf50c86c2d7235da'),
+ 'RFC 2202 #4 (HMAC-MD5/SHA1)'),
+
+ # Test case 5 (MD5)
+ ('0c' * 16,
+ '546573742057697468205472756e636174696f6e',
+ dict(MD5='56461ef2342edc00f9bab995690efd4c'),
+ 'RFC 2202 #5-MD5 (HMAC-MD5)'),
+
+ # Test case 5 (SHA1)
+ # NB: We do not implement hash truncation, so we only test the full hash here.
+ ('0c' * 20,
+ '546573742057697468205472756e636174696f6e',
+ dict(SHA1='4c1a03424b55e07fe7f27be1d58bb9324a9a5a04'),
+ 'RFC 2202 #5-SHA1 (HMAC-SHA1)'),
+
+ # Test case 6
+ ('aa' * 80,
+ '54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a'
+ + '65204b6579202d2048617368204b6579204669727374',
+ dict(MD5='6b1ab7fe4bd7bf8f0b62e6ce61b9d0cd',
+ SHA1='aa4ae5e15272d00e95705637ce8a3b55ed402112'),
+ 'RFC 2202 #6 (HMAC-MD5/SHA1)'),
+
+ # Test case 7
+ ('aa' * 80,
+ '54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a'
+ + '65204b657920616e64204c6172676572205468616e204f6e6520426c6f636b2d'
+ + '53697a652044617461',
+ dict(MD5='6f630fad67cda0ee1fb1f562db3aa53e',
+ SHA1='e8e99d0f45237d786d6bbaa7965c7808bbff1a91'),
+ 'RFC 2202 #7 (HMAC-MD5/SHA1)'),
+
+ ## Test vectors from RFC 4231 ##
+ # 4.2. Test Case 1
+ ('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b',
+ '4869205468657265',
+ dict(SHA256='''
+ b0344c61d8db38535ca8afceaf0bf12b
+ 881dc200c9833da726e9376c2e32cff7
+ '''),
+ 'RFC 4231 #1 (HMAC-SHA256)'),
+
+ # 4.3. Test Case 2 - Test with a key shorter than the length of the HMAC
+ # output.
+ ('4a656665',
+ '7768617420646f2079612077616e7420666f72206e6f7468696e673f',
+ dict(SHA256='''
+ 5bdcc146bf60754e6a042426089575c7
+ 5a003f089d2739839dec58b964ec3843
+ '''),
+ 'RFC 4231 #2 (HMAC-SHA256)'),
+
+ # 4.4. Test Case 3 - Test with a combined length of key and data that is
+ # larger than 64 bytes (= block-size of SHA-224 and SHA-256).
+ ('aa' * 20,
+ 'dd' * 50,
+ dict(SHA256='''
+ 773ea91e36800e46854db8ebd09181a7
+ 2959098b3ef8c122d9635514ced565fe
+ '''),
+ 'RFC 4231 #3 (HMAC-SHA256)'),
+
+ # 4.5. Test Case 4 - Test with a combined length of key and data that is
+ # larger than 64 bytes (= block-size of SHA-224 and SHA-256).
+ ('0102030405060708090a0b0c0d0e0f10111213141516171819',
+ 'cd' * 50,
+ dict(SHA256='''
+ 82558a389a443c0ea4cc819899f2083a
+ 85f0faa3e578f8077a2e3ff46729665b
+ '''),
+ 'RFC 4231 #4 (HMAC-SHA256)'),
+
+ # 4.6. Test Case 5 - Test with a truncation of output to 128 bits.
+ #
+ # Not included because we do not implement hash truncation.
+ #
+
+ # 4.7. Test Case 6 - Test with a key larger than 128 bytes (= block-size of
+ # SHA-384 and SHA-512).
+ ('aa' * 131,
+ '54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a'
+ + '65204b6579202d2048617368204b6579204669727374',
+ dict(SHA256='''
+ 60e431591ee0b67f0d8a26aacbf5b77f
+ 8e0bc6213728c5140546040f0ee37f54
+ '''),
+ 'RFC 4231 #6 (HMAC-SHA256)'),
+
+ # 4.8. Test Case 7 - Test with a key and data that is larger than 128 bytes
+ # (= block-size of SHA-384 and SHA-512).
+ ('aa' * 131,
+ '5468697320697320612074657374207573696e672061206c6172676572207468'
+ + '616e20626c6f636b2d73697a65206b657920616e642061206c61726765722074'
+ + '68616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565'
+ + '647320746f20626520686173686564206265666f7265206265696e6720757365'
+ + '642062792074686520484d414320616c676f726974686d2e',
+ dict(SHA256='''
+ 9b09ffa71b942fcb27635fbcd5b0e944
+ bfdc63644f0713938a7f51535c3a35e2
+ '''),
+ 'RFC 4231 #7 (HMAC-SHA256)'),
+
+ # Test case 8 (SHA224)
+ ('4a656665',
+ '7768617420646f2079612077616e74'
+ + '20666f72206e6f7468696e673f',
+ dict(SHA224='a30e01098bc6dbbf45690f3a7e9e6d0f8bbea2a39e6148008fd05e44'),
+ 'RFC 4634 8.4 SHA224 (HMAC-SHA224)'),
+
+ # Test case 9 (SHA384)
+ ('4a656665',
+ '7768617420646f2079612077616e74'
+ + '20666f72206e6f7468696e673f',
+ dict(SHA384='af45d2e376484031617f78d2b58a6b1b9c7ef464f5a01b47e42ec3736322445e8e2240ca5e69e2c78b3239ecfab21649'),
+ 'RFC 4634 8.4 SHA384 (HMAC-SHA384)'),
+
+ # Test case 10 (SHA512)
+ ('4a656665',
+ '7768617420646f2079612077616e74'
+ + '20666f72206e6f7468696e673f',
+ dict(SHA512='164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737'),
+ 'RFC 4634 8.4 SHA512 (HMAC-SHA512)'),
+
+ # Test case 11 (RIPEMD)
+ ('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b',
+ xl("Hi There"),
+ dict(RIPEMD160='24cb4bd67d20fc1a5d2ed7732dcc39377f0a5668'),
+ 'RFC 2286 #1 (HMAC-RIPEMD)'),
+
+ # Test case 12 (RIPEMD)
+ (xl("Jefe"),
+ xl("what do ya want for nothing?"),
+ dict(RIPEMD160='dda6c0213a485a9e24f4742064a7f033b43c4069'),
+ 'RFC 2286 #2 (HMAC-RIPEMD)'),
+
+ # Test case 13 (RIPEMD)
+ ('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
+ 'dd' * 50,
+ dict(RIPEMD160='b0b105360de759960ab4f35298e116e295d8e7c1'),
+ 'RFC 2286 #3 (HMAC-RIPEMD)'),
+
+ # Test case 14 (RIPEMD)
+ ('0102030405060708090a0b0c0d0e0f10111213141516171819',
+ 'cd' * 50,
+ dict(RIPEMD160='d5ca862f4d21d5e610e18b4cf1beb97a4365ecf4'),
+ 'RFC 2286 #4 (HMAC-RIPEMD)'),
+
+ # Test case 15 (RIPEMD)
+ ('0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c',
+ xl("Test With Truncation"),
+ dict(RIPEMD160='7619693978f91d90539ae786500ff3d8e0518e39'),
+ 'RFC 2286 #5 (HMAC-RIPEMD)'),
+
+ # Test case 16 (RIPEMD)
+ ('aa' * 80,
+ xl("Test Using Larger Than Block-Size Key - Hash Key First"),
+ dict(RIPEMD160='6466ca07ac5eac29e1bd523e5ada7605b791fd8b'),
+ 'RFC 2286 #6 (HMAC-RIPEMD)'),
+
+ # Test case 17 (RIPEMD)
+ ('aa' * 80,
+ xl("Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data"),
+ dict(RIPEMD160='69ea60798d71616cce5fd0871e23754cd75d5a0a'),
+ 'RFC 2286 #7 (HMAC-RIPEMD)'),
+
+ # From https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/HMAC_SHA3-224.pdf
+ (
+ '000102030405060708090a0b0c0d0e0f'
+ '101112131415161718191a1b',
+ xl('Sample message for keylenblocklen'),
+ dict(SHA3_224='078695eecc227c636ad31d063a15dd05a7e819a66ec6d8de1e193e59'),
+ 'NIST CSRC Sample #3 (SHA3-224)'
+ ),
+
+ # From https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/HMAC_SHA3-256.pdf
+ (
+ '000102030405060708090a0b0c0d0e0f'\
+ '101112131415161718191a1b1c1d1e1f',
+ xl('Sample message for keylenblocklen'),
+ dict(SHA3_256='9bcf2c238e235c3ce88404e813bd2f3a97185ac6f238c63d6229a00b07974258'),
+ 'NIST CSRC Sample #3 (SHA3-256)'
+ ),
+
+ # From https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/HMAC_SHA3-384.pdf
+ (
+ '000102030405060708090a0b0c0d0e0f'\
+ '101112131415161718191a1b1c1d1e1f'
+ '202122232425262728292a2b2c2d2e2f',
+ xl('Sample message for keylenblocklen'),
+ dict(SHA3_384='e5ae4c739f455279368ebf36d4f5354c95aa184c899d3870e460ebc288ef1f9470053f73f7c6da2a71bcaec38ce7d6ac'),
+ 'NIST CSRC Sample #3 (SHA3-384)'
+ ),
+
+ # From https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/HMAC_SHA3-512.pdf
+ (
+ '000102030405060708090a0b0c0d0e0f'\
+ '101112131415161718191a1b1c1d1e1f'\
+ '202122232425262728292a2b2c2d2e2f'\
+ '303132333435363738393a3b3c3d3e3f',
+ xl('Sample message for keylenblocklen'),
+ dict(SHA3_512='5f464f5e5b7848e3885e49b2c385f0694985d0e38966242dc4a5fe3fea4b37d46b65ceced5dcf59438dd840bab22269f0ba7febdb9fcf74602a35666b2a32915'),
+ 'NIST CSRC Sample #3 (SHA3-512)'
+ ),
+
+]
+
+
+class HMAC_Module_and_Instance_Test(unittest.TestCase):
+ """Test the HMAC construction and verify that it does not
+ matter if you initialize it with a hash module or
+ with an hash instance.
+
+ See https://bugs.launchpad.net/pycrypto/+bug/1209399
+ """
+
+ def __init__(self, hashmods):
+ """Initialize the test with a dictionary of hash modules
+ indexed by their names"""
+
+ unittest.TestCase.__init__(self)
+ self.hashmods = hashmods
+ self.description = ""
+
+ def shortDescription(self):
+ return self.description
+
+ def runTest(self):
+ key = b"\x90\x91\x92\x93" * 4
+ payload = b"\x00" * 100
+
+ for hashname, hashmod in self.hashmods.items():
+ if hashmod is None:
+ continue
+ self.description = "Test HMAC in combination with " + hashname
+ one = HMAC.new(key, payload, hashmod).digest()
+ two = HMAC.new(key, payload, hashmod.new()).digest()
+ self.assertEqual(one, two)
+
+
+class HMAC_None(unittest.TestCase):
+
+ def runTest(self):
+
+ key = b"\x04" * 20
+ one = HMAC.new(key, b"", SHA1).digest()
+ two = HMAC.new(key, None, SHA1).digest()
+ self.assertEqual(one, two)
+
+
+class ByteArrayTests(unittest.TestCase):
+
+ def runTest(self):
+
+ key = b"0" * 16
+ data = b"\x00\x01\x02"
+
+ # Data and key can be a bytearray (during initialization)
+ key_ba = bytearray(key)
+ data_ba = bytearray(data)
+
+ h1 = HMAC.new(key, data)
+ h2 = HMAC.new(key_ba, data_ba)
+ key_ba[:1] = b'\xFF'
+ data_ba[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a bytearray (during operation)
+ key_ba = bytearray(key)
+ data_ba = bytearray(data)
+
+ h1 = HMAC.new(key)
+ h2 = HMAC.new(key)
+ h1.update(data)
+ h2.update(data_ba)
+ data_ba[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+
+class MemoryViewTests(unittest.TestCase):
+
+ def runTest(self):
+
+ key = b"0" * 16
+ data = b"\x00\x01\x02"
+
+ def get_mv_ro(data):
+ return memoryview(data)
+
+ def get_mv_rw(data):
+ return memoryview(bytearray(data))
+
+ for get_mv in (get_mv_ro, get_mv_rw):
+
+ # Data and key can be a memoryview (during initialization)
+ key_mv = get_mv(key)
+ data_mv = get_mv(data)
+
+ h1 = HMAC.new(key, data)
+ h2 = HMAC.new(key_mv, data_mv)
+ if not data_mv.readonly:
+ key_mv[:1] = b'\xFF'
+ data_mv[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a memoryview (during operation)
+ data_mv = get_mv(data)
+
+ h1 = HMAC.new(key)
+ h2 = HMAC.new(key)
+ h1.update(data)
+ h2.update(data_mv)
+ if not data_mv.readonly:
+ data_mv[:1] = b'\xFF'
+ self.assertEqual(h1.digest(), h2.digest())
+
+
+def get_tests(config={}):
+ global test_data
+ import types
+ from .common import make_mac_tests
+
+ # A test vector contains multiple results, each one for a
+ # different hash algorithm.
+ # Here we expand each test vector into multiple ones,
+ # and add the relevant parameters that will be passed to new()
+ exp_test_data = []
+ for row in test_data:
+ for modname in row[2].keys():
+ t = list(row)
+ t[2] = row[2][modname]
+ t.append(dict(digestmod=globals()[modname]))
+ exp_test_data.append(t)
+ tests = make_mac_tests(HMAC, "HMAC", exp_test_data)
+ tests.append(HMAC_Module_and_Instance_Test(hash_modules))
+ tests.append(HMAC_None())
+
+ tests.append(ByteArrayTests())
+ tests.append(MemoryViewTests())
+
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_KMAC.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_KMAC.py
new file mode 100644
index 00000000..8e9bf702
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_KMAC.py
@@ -0,0 +1,346 @@
+import unittest
+from binascii import unhexlify, hexlify
+
+from Crypto.Util.py3compat import tobytes
+from Crypto.Util.strxor import strxor_c
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Hash import KMAC128, KMAC256
+
+
+class KMACTest(unittest.TestCase):
+
+ def new(self, *args, **kwargs):
+ return self.KMAC.new(key=b'X' * (self.minimum_key_bits // 8), *args, **kwargs)
+
+ def test_new_positive(self):
+
+ key = b'X' * 32
+
+ h = self.new()
+ for new_func in self.KMAC.new, h.new:
+
+ for dbytes in range(self.minimum_bytes, 128 + 1):
+ hobj = new_func(key=key, mac_len=dbytes)
+ self.assertEqual(hobj.digest_size, dbytes)
+
+ digest1 = new_func(key=key, data=b"\x90").digest()
+ digest2 = new_func(key=key).update(b"\x90").digest()
+ self.assertEqual(digest1, digest2)
+
+ new_func(data=b"A", key=key, custom=b"g")
+
+ hobj = h.new(key=key)
+ self.assertEqual(hobj.digest_size, self.default_bytes)
+
+ def test_new_negative(self):
+
+ h = self.new()
+ for new_func in self.KMAC.new, h.new:
+ self.assertRaises(ValueError, new_func, key=b'X'*32,
+ mac_len=0)
+ self.assertRaises(ValueError, new_func, key=b'X'*32,
+ mac_len=self.minimum_bytes - 1)
+ self.assertRaises(TypeError, new_func,
+ key=u"string")
+ self.assertRaises(TypeError, new_func,
+ data=u"string")
+
+ def test_default_digest_size(self):
+ digest = self.new(data=b'abc').digest()
+ self.assertEqual(len(digest), self.default_bytes)
+
+ def test_update(self):
+ pieces = [b"\x0A" * 200, b"\x14" * 300]
+ h = self.new()
+ h.update(pieces[0]).update(pieces[1])
+ digest = h.digest()
+ h = self.new()
+ h.update(pieces[0] + pieces[1])
+ self.assertEqual(h.digest(), digest)
+
+ def test_update_negative(self):
+ h = self.new()
+ self.assertRaises(TypeError, h.update, u"string")
+
+ def test_digest(self):
+ h = self.new()
+ digest = h.digest()
+
+ # hexdigest does not change the state
+ self.assertEqual(h.digest(), digest)
+ # digest returns a byte string
+ self.assertTrue(isinstance(digest, type(b"digest")))
+
+ def test_update_after_digest(self):
+ msg = b"rrrrttt"
+
+ # Normally, update() cannot be done after digest()
+ h = self.new(mac_len=32, data=msg[:4])
+ dig1 = h.digest()
+ self.assertRaises(TypeError, h.update, dig1)
+
+ def test_hex_digest(self):
+ mac = self.new()
+ digest = mac.digest()
+ hexdigest = mac.hexdigest()
+
+ # hexdigest is equivalent to digest
+ self.assertEqual(hexlify(digest), tobytes(hexdigest))
+ # hexdigest does not change the state
+ self.assertEqual(mac.hexdigest(), hexdigest)
+ # hexdigest returns a string
+ self.assertTrue(isinstance(hexdigest, type("digest")))
+
+ def test_verify(self):
+ h = self.new()
+ mac = h.digest()
+ h.verify(mac)
+ wrong_mac = strxor_c(mac, 255)
+ self.assertRaises(ValueError, h.verify, wrong_mac)
+
+ def test_hexverify(self):
+ h = self.new()
+ mac = h.hexdigest()
+ h.hexverify(mac)
+ self.assertRaises(ValueError, h.hexverify, "4556")
+
+ def test_oid(self):
+
+ oid = "2.16.840.1.101.3.4.2." + self.oid_variant
+ h = self.new()
+ self.assertEqual(h.oid, oid)
+
+ def test_bytearray(self):
+
+ key = b'0' * 32
+ data = b"\x00\x01\x02"
+
+ # Data and key can be a bytearray (during initialization)
+ key_ba = bytearray(key)
+ data_ba = bytearray(data)
+
+ h1 = self.KMAC.new(data=data, key=key)
+ h2 = self.KMAC.new(data=data_ba, key=key_ba)
+ key_ba[:1] = b'\xFF'
+ data_ba[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a bytearray (during operation)
+ data_ba = bytearray(data)
+
+ h1 = self.new()
+ h2 = self.new()
+ h1.update(data)
+ h2.update(data_ba)
+ data_ba[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+ def test_memoryview(self):
+
+ key = b'0' * 32
+ data = b"\x00\x01\x02"
+
+ def get_mv_ro(data):
+ return memoryview(data)
+
+ def get_mv_rw(data):
+ return memoryview(bytearray(data))
+
+ for get_mv in (get_mv_ro, get_mv_rw):
+
+ # Data and key can be a memoryview (during initialization)
+ key_mv = get_mv(key)
+ data_mv = get_mv(data)
+
+ h1 = self.KMAC.new(data=data, key=key)
+ h2 = self.KMAC.new(data=data_mv, key=key_mv)
+ if not data_mv.readonly:
+ data_mv[:1] = b'\xFF'
+ key_mv[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a memoryview (during operation)
+ data_mv = get_mv(data)
+
+ h1 = self.new()
+ h2 = self.new()
+ h1.update(data)
+ h2.update(data_mv)
+ if not data_mv.readonly:
+ data_mv[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+
+class KMAC128Test(KMACTest):
+
+ KMAC = KMAC128
+
+ minimum_key_bits = 128
+
+ minimum_bytes = 8
+ default_bytes = 64
+
+ oid_variant = "19"
+
+
+class KMAC256Test(KMACTest):
+
+ KMAC = KMAC256
+
+ minimum_key_bits = 256
+
+ minimum_bytes = 8
+ default_bytes = 64
+
+ oid_variant = "20"
+
+
+class NISTExampleTestVectors(unittest.TestCase):
+
+ # https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/KMAC_samples.pdf
+ test_data = [
+ (
+ "40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F"
+ "50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F",
+ "00 01 02 03",
+ "",
+ "E5 78 0B 0D 3E A6 F7 D3 A4 29 C5 70 6A A4 3A 00"
+ "FA DB D7 D4 96 28 83 9E 31 87 24 3F 45 6E E1 4E",
+ "Sample #1 NIST",
+ KMAC128
+ ),
+ (
+ "40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F"
+ "50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F",
+ "00 01 02 03",
+ "My Tagged Application",
+ "3B 1F BA 96 3C D8 B0 B5 9E 8C 1A 6D 71 88 8B 71"
+ "43 65 1A F8 BA 0A 70 70 C0 97 9E 28 11 32 4A A5",
+ "Sample #2 NIST",
+ KMAC128
+ ),
+ (
+ "40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F"
+ "50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F",
+ "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+ "10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+ "20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F"
+ "30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F"
+ "40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F"
+ "50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F"
+ "60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F"
+ "70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F"
+ "80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F"
+ "90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F"
+ "A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF"
+ "B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF"
+ "C0 C1 C2 C3 C4 C5 C6 C7",
+ "My Tagged Application",
+ "1F 5B 4E 6C CA 02 20 9E 0D CB 5C A6 35 B8 9A 15"
+ "E2 71 EC C7 60 07 1D FD 80 5F AA 38 F9 72 92 30",
+ "Sample #3 NIST",
+ KMAC128
+ ),
+ (
+ "40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F"
+ "50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F",
+ "00 01 02 03",
+ "My Tagged Application",
+ "20 C5 70 C3 13 46 F7 03 C9 AC 36 C6 1C 03 CB 64"
+ "C3 97 0D 0C FC 78 7E 9B 79 59 9D 27 3A 68 D2 F7"
+ "F6 9D 4C C3 DE 9D 10 4A 35 16 89 F2 7C F6 F5 95"
+ "1F 01 03 F3 3F 4F 24 87 10 24 D9 C2 77 73 A8 DD",
+ "Sample #4 NIST",
+ KMAC256
+ ),
+ (
+ "40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F"
+ "50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F",
+ "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+ "10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+ "20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F"
+ "30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F"
+ "40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F"
+ "50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F"
+ "60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F"
+ "70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F"
+ "80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F"
+ "90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F"
+ "A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF"
+ "B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF"
+ "C0 C1 C2 C3 C4 C5 C6 C7",
+ "",
+ "75 35 8C F3 9E 41 49 4E 94 97 07 92 7C EE 0A F2"
+ "0A 3F F5 53 90 4C 86 B0 8F 21 CC 41 4B CF D6 91"
+ "58 9D 27 CF 5E 15 36 9C BB FF 8B 9A 4C 2E B1 78"
+ "00 85 5D 02 35 FF 63 5D A8 25 33 EC 6B 75 9B 69",
+ "Sample #5 NIST",
+ KMAC256
+ ),
+ (
+ "40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F"
+ "50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F",
+ "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
+ "10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
+ "20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F"
+ "30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F"
+ "40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F"
+ "50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F"
+ "60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F"
+ "70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F"
+ "80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F"
+ "90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F"
+ "A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF"
+ "B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF"
+ "C0 C1 C2 C3 C4 C5 C6 C7",
+ "My Tagged Application",
+ "B5 86 18 F7 1F 92 E1 D5 6C 1B 8C 55 DD D7 CD 18"
+ "8B 97 B4 CA 4D 99 83 1E B2 69 9A 83 7D A2 E4 D9"
+ "70 FB AC FD E5 00 33 AE A5 85 F1 A2 70 85 10 C3"
+ "2D 07 88 08 01 BD 18 28 98 FE 47 68 76 FC 89 65",
+ "Sample #6 NIST",
+ KMAC256
+ ),
+ ]
+
+ def setUp(self):
+ td = []
+ for key, data, custom, mac, text, module in self.test_data:
+ ni = (
+ unhexlify(key.replace(" ", "")),
+ unhexlify(data.replace(" ", "")),
+ custom.encode(),
+ unhexlify(mac.replace(" ", "")),
+ text,
+ module
+ )
+ td.append(ni)
+ self.test_data = td
+
+ def runTest(self):
+
+ for key, data, custom, mac, text, module in self.test_data:
+ h = module.new(data=data, key=key, custom=custom, mac_len=len(mac))
+ mac_tag = h.digest()
+ self.assertEqual(mac_tag, mac, msg=text)
+
+
+def get_tests(config={}):
+ tests = []
+
+ tests += list_test_cases(KMAC128Test)
+ tests += list_test_cases(KMAC256Test)
+ tests.append(NISTExampleTestVectors())
+
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_KangarooTwelve.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_KangarooTwelve.py
new file mode 100644
index 00000000..fe161f06
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_KangarooTwelve.py
@@ -0,0 +1,367 @@
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.KangarooTwelve"""
+
+import unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Hash import KangarooTwelve as K12
+from Crypto.Util.py3compat import b, bchr
+
+
+class KangarooTwelveTest(unittest.TestCase):
+
+ def test_length_encode(self):
+ self.assertEqual(K12._length_encode(0), b'\x00')
+ self.assertEqual(K12._length_encode(12), b'\x0C\x01')
+ self.assertEqual(K12._length_encode(65538), b'\x01\x00\x02\x03')
+
+ def test_new_positive(self):
+
+ xof1 = K12.new()
+ xof2 = K12.new(data=b("90"))
+ xof3 = K12.new().update(b("90"))
+
+ self.assertNotEqual(xof1.read(10), xof2.read(10))
+ xof3.read(10)
+ self.assertEqual(xof2.read(10), xof3.read(10))
+
+ xof1 = K12.new()
+ ref = xof1.read(10)
+ xof2 = K12.new(custom=b(""))
+ xof3 = K12.new(custom=b("foo"))
+
+ self.assertEqual(ref, xof2.read(10))
+ self.assertNotEqual(ref, xof3.read(10))
+
+ xof1 = K12.new(custom=b("foo"))
+ xof2 = K12.new(custom=b("foo"), data=b("90"))
+ xof3 = K12.new(custom=b("foo")).update(b("90"))
+
+ self.assertNotEqual(xof1.read(10), xof2.read(10))
+ xof3.read(10)
+ self.assertEqual(xof2.read(10), xof3.read(10))
+
+ def test_update(self):
+ pieces = [bchr(10) * 200, bchr(20) * 300]
+ h = K12.new()
+ h.update(pieces[0]).update(pieces[1])
+ digest = h.read(10)
+ h = K12.new()
+ h.update(pieces[0] + pieces[1])
+ self.assertEqual(h.read(10), digest)
+
+ def test_update_negative(self):
+ h = K12.new()
+ self.assertRaises(TypeError, h.update, u"string")
+
+ def test_digest(self):
+ h = K12.new()
+ digest = h.read(90)
+
+ # read returns a byte string of the right length
+ self.assertTrue(isinstance(digest, type(b("digest"))))
+ self.assertEqual(len(digest), 90)
+
+ def test_update_after_read(self):
+ mac = K12.new()
+ mac.update(b("rrrr"))
+ mac.read(90)
+ self.assertRaises(TypeError, mac.update, b("ttt"))
+
+
+def txt2bin(txt):
+ clean = txt.replace(" ", "").replace("\n", "").replace("\r", "")
+ return unhexlify(clean)
+
+
+def ptn(n):
+ res = bytearray(n)
+ pattern = b"".join([bchr(x) for x in range(0, 0xFB)])
+ for base in range(0, n - 0xFB, 0xFB):
+ res[base:base + 0xFB] = pattern
+ remain = n % 0xFB
+ if remain:
+ base = (n // 0xFB) * 0xFB
+ res[base:] = pattern[:remain]
+ assert(len(res) == n)
+ return res
+
+
+def chunked(source, size):
+ for i in range(0, len(source), size):
+ yield source[i:i+size]
+
+
+class KangarooTwelveTV(unittest.TestCase):
+
+ # https://github.com/XKCP/XKCP/blob/master/tests/TestVectors/KangarooTwelve.txt
+
+ def test_zero_1(self):
+ tv = """1A C2 D4 50 FC 3B 42 05 D1 9D A7 BF CA 1B 37 51
+ 3C 08 03 57 7A C7 16 7F 06 FE 2C E1 F0 EF 39 E5"""
+
+ btv = txt2bin(tv)
+ res = K12.new().read(32)
+ self.assertEqual(res, btv)
+
+ def test_zero_2(self):
+ tv = """1A C2 D4 50 FC 3B 42 05 D1 9D A7 BF CA 1B 37 51
+ 3C 08 03 57 7A C7 16 7F 06 FE 2C E1 F0 EF 39 E5
+ 42 69 C0 56 B8 C8 2E 48 27 60 38 B6 D2 92 96 6C
+ C0 7A 3D 46 45 27 2E 31 FF 38 50 81 39 EB 0A 71"""
+
+ btv = txt2bin(tv)
+ res = K12.new().read(64)
+ self.assertEqual(res, btv)
+
+ def test_zero_3(self):
+ tv = """E8 DC 56 36 42 F7 22 8C 84 68 4C 89 84 05 D3 A8
+ 34 79 91 58 C0 79 B1 28 80 27 7A 1D 28 E2 FF 6D"""
+
+ btv = txt2bin(tv)
+ res = K12.new().read(10032)
+ self.assertEqual(res[-32:], btv)
+
+ def test_ptn_1(self):
+ tv = """2B DA 92 45 0E 8B 14 7F 8A 7C B6 29 E7 84 A0 58
+ EF CA 7C F7 D8 21 8E 02 D3 45 DF AA 65 24 4A 1F"""
+
+ btv = txt2bin(tv)
+ res = K12.new(data=ptn(1)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17(self):
+ tv = """6B F7 5F A2 23 91 98 DB 47 72 E3 64 78 F8 E1 9B
+ 0F 37 12 05 F6 A9 A9 3A 27 3F 51 DF 37 12 28 88"""
+
+ btv = txt2bin(tv)
+ res = K12.new(data=ptn(17)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_2(self):
+ tv = """0C 31 5E BC DE DB F6 14 26 DE 7D CF 8F B7 25 D1
+ E7 46 75 D7 F5 32 7A 50 67 F3 67 B1 08 EC B6 7C"""
+
+ btv = txt2bin(tv)
+ res = K12.new(data=ptn(17**2)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_3(self):
+ tv = """CB 55 2E 2E C7 7D 99 10 70 1D 57 8B 45 7D DF 77
+ 2C 12 E3 22 E4 EE 7F E4 17 F9 2C 75 8F 0D 59 D0"""
+
+ btv = txt2bin(tv)
+ res = K12.new(data=ptn(17**3)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_4(self):
+ tv = """87 01 04 5E 22 20 53 45 FF 4D DA 05 55 5C BB 5C
+ 3A F1 A7 71 C2 B8 9B AE F3 7D B4 3D 99 98 B9 FE"""
+
+ btv = txt2bin(tv)
+ data = ptn(17**4)
+
+ # All at once
+ res = K12.new(data=data).read(32)
+ self.assertEqual(res, btv)
+
+ # Byte by byte
+ k12 = K12.new()
+ for x in data:
+ k12.update(bchr(x))
+ res = k12.read(32)
+ self.assertEqual(res, btv)
+
+ # Chunks of various prime sizes
+ for chunk_size in (13, 17, 19, 23, 31):
+ k12 = K12.new()
+ for x in chunked(data, chunk_size):
+ k12.update(x)
+ res = k12.read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_5(self):
+ tv = """84 4D 61 09 33 B1 B9 96 3C BD EB 5A E3 B6 B0 5C
+ C7 CB D6 7C EE DF 88 3E B6 78 A0 A8 E0 37 16 82"""
+
+ btv = txt2bin(tv)
+ data = ptn(17**5)
+
+ # All at once
+ res = K12.new(data=data).read(32)
+ self.assertEqual(res, btv)
+
+ # Chunks
+ k12 = K12.new()
+ for chunk in chunked(data, 8192):
+ k12.update(chunk)
+ res = k12.read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_6(self):
+ tv = """3C 39 07 82 A8 A4 E8 9F A6 36 7F 72 FE AA F1 32
+ 55 C8 D9 58 78 48 1D 3C D8 CE 85 F5 8E 88 0A F8"""
+
+ btv = txt2bin(tv)
+ data = ptn(17**6)
+
+ # All at once
+ res = K12.new(data=data).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_c_1(self):
+ tv = """FA B6 58 DB 63 E9 4A 24 61 88 BF 7A F6 9A 13 30
+ 45 F4 6E E9 84 C5 6E 3C 33 28 CA AF 1A A1 A5 83"""
+
+ btv = txt2bin(tv)
+ custom = ptn(1)
+
+ # All at once
+ res = K12.new(custom=custom).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_c_41(self):
+ tv = """D8 48 C5 06 8C ED 73 6F 44 62 15 9B 98 67 FD 4C
+ 20 B8 08 AC C3 D5 BC 48 E0 B0 6B A0 A3 76 2E C4"""
+
+ btv = txt2bin(tv)
+ custom = ptn(41)
+
+ # All at once
+ res = K12.new(data=b'\xFF', custom=custom).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_c_41_2(self):
+ tv = """C3 89 E5 00 9A E5 71 20 85 4C 2E 8C 64 67 0A C0
+ 13 58 CF 4C 1B AF 89 44 7A 72 42 34 DC 7C ED 74"""
+
+ btv = txt2bin(tv)
+ custom = ptn(41**2)
+
+ # All at once
+ res = K12.new(data=b'\xFF' * 3, custom=custom).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_c_41_3(self):
+ tv = """75 D2 F8 6A 2E 64 45 66 72 6B 4F BC FC 56 57 B9
+ DB CF 07 0C 7B 0D CA 06 45 0A B2 91 D7 44 3B CF"""
+
+ btv = txt2bin(tv)
+ custom = ptn(41**3)
+
+ # All at once
+ res = K12.new(data=b'\xFF' * 7, custom=custom).read(32)
+ self.assertEqual(res, btv)
+
+ # https://datatracker.ietf.org/doc/draft-irtf-cfrg-kangarootwelve/
+
+ def test_ptn_8191(self):
+ tv = """1B 57 76 36 F7 23 64 3E 99 0C C7 D6 A6 59 83 74
+ 36 FD 6A 10 36 26 60 0E B8 30 1C D1 DB E5 53 D6"""
+
+ btv = txt2bin(tv)
+
+ # All at once
+ res = K12.new(data=ptn(8191)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_8192(self):
+ tv = """48 F2 56 F6 77 2F 9E DF B6 A8 B6 61 EC 92 DC 93
+ B9 5E BD 05 A0 8A 17 B3 9A E3 49 08 70 C9 26 C3"""
+
+ btv = txt2bin(tv)
+
+ # All at once
+ res = K12.new(data=ptn(8192)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_8192_8189(self):
+ tv = """3E D1 2F 70 FB 05 DD B5 86 89 51 0A B3 E4 D2 3C
+ 6C 60 33 84 9A A0 1E 1D 8C 22 0A 29 7F ED CD 0B"""
+
+ btv = txt2bin(tv)
+
+ # All at once
+ res = K12.new(data=ptn(8192), custom=ptn(8189)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_8192_8190(self):
+ tv = """6A 7C 1B 6A 5C D0 D8 C9 CA 94 3A 4A 21 6C C6 46
+ 04 55 9A 2E A4 5F 78 57 0A 15 25 3D 67 BA 00 AE"""
+
+ btv = txt2bin(tv)
+
+ # All at once
+ res = K12.new(data=ptn(8192), custom=ptn(8190)).read(32)
+ self.assertEqual(res, btv)
+
+ ###
+
+ def test_1(self):
+ tv = "fd608f91d81904a9916e78a18f65c157a78d63f93d8f6367db0524526a5ea2bb"
+
+ btv = txt2bin(tv)
+ res = K12.new(data=b'', custom=ptn(100)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_2(self):
+ tv4 = "5a4ec9a649f81916d4ce1553492962f7868abf8dd1ceb2f0cb3682ea95cda6a6"
+ tv3 = "441688fe4fe4ae9425eb3105eb445eb2b3a6f67b66eff8e74ebfbc49371f6d4c"
+ tv2 = "17269a57759af0214c84a0fd9bc851f4d95f80554cfed4e7da8a6ee1ff080131"
+ tv1 = "33826990c09dc712ba7224f0d9be319e2720de95a4c1afbd2211507dae1c703a"
+ tv0 = "9f4d3aba908ddc096e4d3a71da954f917b9752f05052b9d26d916a6fbc75bf3e"
+
+ res = K12.new(data=b'A' * (8192 - 4), custom=b'B').read(32)
+ self.assertEqual(res, txt2bin(tv4))
+
+ res = K12.new(data=b'A' * (8192 - 3), custom=b'B').read(32)
+ self.assertEqual(res, txt2bin(tv3))
+
+ res = K12.new(data=b'A' * (8192 - 2), custom=b'B').read(32)
+ self.assertEqual(res, txt2bin(tv2))
+
+ res = K12.new(data=b'A' * (8192 - 1), custom=b'B').read(32)
+ self.assertEqual(res, txt2bin(tv1))
+
+ res = K12.new(data=b'A' * (8192 - 0), custom=b'B').read(32)
+ self.assertEqual(res, txt2bin(tv0))
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(KangarooTwelveTest)
+ tests += list_test_cases(KangarooTwelveTV)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_MD2.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_MD2.py
new file mode 100644
index 00000000..93751687
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_MD2.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/MD2.py: Self-test for the MD2 hash function
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.MD2"""
+
+from Crypto.Util.py3compat import *
+
+# This is a list of (expected_result, input[, description]) tuples.
+test_data = [
+ # Test vectors from RFC 1319
+ ('8350e5a3e24c153df2275c9f80692773', '', "'' (empty string)"),
+ ('32ec01ec4a6dac72c0ab96fb34c0b5d1', 'a'),
+ ('da853b0d3f88d99b30283a69e6ded6bb', 'abc'),
+ ('ab4f496bfb2a530b219ff33031fe06b0', 'message digest'),
+
+ ('4e8ddff3650292ab5a4108c3aa47940b', 'abcdefghijklmnopqrstuvwxyz',
+ 'a-z'),
+
+ ('da33def2a42df13975352846c30338cd',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
+ 'A-Z, a-z, 0-9'),
+
+ ('d5976f79d83d3a0dc9806c3c66f3efd8',
+ '1234567890123456789012345678901234567890123456'
+ + '7890123456789012345678901234567890',
+ "'1234567890' * 8"),
+]
+
+def get_tests(config={}):
+ from Crypto.Hash import MD2
+ from .common import make_hash_tests
+ return make_hash_tests(MD2, "MD2", test_data,
+ digest_size=16,
+ oid="1.2.840.113549.2.2")
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_MD4.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_MD4.py
new file mode 100644
index 00000000..17b48a77
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_MD4.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/MD4.py: Self-test for the MD4 hash function
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.MD4"""
+
+__revision__ = "$Id$"
+
+from Crypto.Util.py3compat import *
+
+# This is a list of (expected_result, input[, description]) tuples.
+test_data = [
+ # Test vectors from RFC 1320
+ ('31d6cfe0d16ae931b73c59d7e0c089c0', '', "'' (empty string)"),
+ ('bde52cb31de33e46245e05fbdbd6fb24', 'a'),
+ ('a448017aaf21d8525fc10ae87aa6729d', 'abc'),
+ ('d9130a8164549fe818874806e1c7014b', 'message digest'),
+
+ ('d79e1c308aa5bbcdeea8ed63df412da9', 'abcdefghijklmnopqrstuvwxyz',
+ 'a-z'),
+
+ ('043f8582f241db351ce627e153e7f0e4',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
+ 'A-Z, a-z, 0-9'),
+
+ ('e33b4ddc9c38f2199c3e7b164fcc0536',
+ '1234567890123456789012345678901234567890123456'
+ + '7890123456789012345678901234567890',
+ "'1234567890' * 8"),
+]
+
+def get_tests(config={}):
+ from Crypto.Hash import MD4
+ from .common import make_hash_tests
+ return make_hash_tests(MD4, "MD4", test_data,
+ digest_size=16,
+ oid="1.2.840.113549.2.4")
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_MD5.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_MD5.py
new file mode 100644
index 00000000..830ace73
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_MD5.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/MD5.py: Self-test for the MD5 hash function
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.MD5"""
+
+from Crypto.Util.py3compat import *
+from Crypto.Hash import MD5
+from binascii import unhexlify
+import unittest
+from Crypto.SelfTest.st_common import list_test_cases
+
+
+# This is a list of (expected_result, input[, description]) tuples.
+test_data = [
+ # Test vectors from RFC 1321
+ ('d41d8cd98f00b204e9800998ecf8427e', '', "'' (empty string)"),
+ ('0cc175b9c0f1b6a831c399e269772661', 'a'),
+ ('900150983cd24fb0d6963f7d28e17f72', 'abc'),
+ ('f96b697d7cb7938d525a2f31aaf161d0', 'message digest'),
+
+ ('c3fcd3d76192e4007dfb496cca67e13b', 'abcdefghijklmnopqrstuvwxyz',
+ 'a-z'),
+
+ ('d174ab98d277d9f5a5611c2c9f419d9f',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
+ 'A-Z, a-z, 0-9'),
+
+ ('57edf4a22be3c955ac49da2e2107b67a',
+ '1234567890123456789012345678901234567890123456'
+ + '7890123456789012345678901234567890',
+ "'1234567890' * 8"),
+
+ # https://www.cosic.esat.kuleuven.be/nessie/testvectors/hash/md5/Md5-128.unverified.test-vectors
+ ('57EDF4A22BE3C955AC49DA2E2107B67A', '1234567890' * 8, 'Set 1, vector #7'),
+ ('7707D6AE4E027C70EEA2A935C2296F21', 'a'*1000000, 'Set 1, vector #8'),
+]
+
+
+class Md5IterTest(unittest.TestCase):
+
+ def runTest(self):
+ message = b("\x00") * 16
+ result1 = "4AE71336E44BF9BF79D2752E234818A5".lower()
+ result2 = "1A83F51285E4D89403D00C46EF8508FE".lower()
+
+ h = MD5.new(message)
+ message = h.digest()
+ self.assertEqual(h.hexdigest(), result1)
+
+ for _ in range(99999):
+ h = MD5.new(message)
+ message = h.digest()
+
+ self.assertEqual(h.hexdigest(), result2)
+
+
+def get_tests(config={}):
+ from .common import make_hash_tests
+
+ tests = make_hash_tests(MD5, "MD5", test_data,
+ digest_size=16,
+ oid="1.2.840.113549.2.5")
+ if config.get('slow_tests'):
+ tests += [ Md5IterTest() ]
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_Poly1305.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_Poly1305.py
new file mode 100644
index 00000000..0612d4ed
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_Poly1305.py
@@ -0,0 +1,542 @@
+#
+# SelfTest/Hash/test_Poly1305.py: Self-test for the Poly1305 module
+#
+# ===================================================================
+#
+# Copyright (c) 2018, Helder Eijs
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash._Poly1305"""
+
+import json
+import unittest
+from binascii import unhexlify, hexlify
+
+from .common import make_mac_tests
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Hash import Poly1305
+from Crypto.Cipher import AES, ChaCha20
+
+from Crypto.Util.py3compat import tobytes
+from Crypto.Util.strxor import strxor_c
+
+# This is a list of (r+s keypair, data, result, description, keywords) tuples.
+test_data_basic = [
+ (
+ "85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b",
+ hexlify(b"Cryptographic Forum Research Group").decode(),
+ "a8061dc1305136c6c22b8baf0c0127a9",
+ "RFC7539"
+ ),
+ (
+ "746869732069732033322d62797465206b657920666f7220506f6c7931333035",
+ "0000000000000000000000000000000000000000000000000000000000000000",
+ "49ec78090e481ec6c26b33b91ccc0307",
+ "https://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-00#section-7 A",
+ ),
+ (
+ "746869732069732033322d62797465206b657920666f7220506f6c7931333035",
+ "48656c6c6f20776f726c6421",
+ "a6f745008f81c916a20dcc74eef2b2f0",
+ "https://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-00#section-7 B",
+ ),
+ (
+ "746869732069732033322d62797465206b657920666f7220506f6c7931333035",
+ "",
+ "6b657920666f7220506f6c7931333035",
+ "Generated with pure Python",
+ ),
+ (
+ "746869732069732033322d62797465206b657920666f7220506f6c7931333035",
+ "FF",
+ "f7e4e0ef4c46d106219da3d1bdaeb3ff",
+ "Generated with pure Python",
+ ),
+ (
+ "746869732069732033322d62797465206b657920666f7220506f6c7931333035",
+ "FF00",
+ "7471eceeb22988fc936da1d6e838b70e",
+ "Generated with pure Python",
+ ),
+ (
+ "746869732069732033322d62797465206b657920666f7220506f6c7931333035",
+ "AA" * 17,
+ "32590bc07cb2afaccca3f67f122975fe",
+ "Generated with pure Python",
+ ),
+ (
+ "00" * 32,
+ "00" * 64,
+ "00" * 16,
+ "RFC7539 A.3 #1",
+ ),
+ (
+ "0000000000000000000000000000000036e5f6b5c5e06070f0efca96227a863e",
+ hexlify(
+ b"Any submission t"
+ b"o the IETF inten"
+ b"ded by the Contr"
+ b"ibutor for publi"
+ b"cation as all or"
+ b" part of an IETF"
+ b" Internet-Draft "
+ b"or RFC and any s"
+ b"tatement made wi"
+ b"thin the context"
+ b" of an IETF acti"
+ b"vity is consider"
+ b"ed an \"IETF Cont"
+ b"ribution\". Such "
+ b"statements inclu"
+ b"de oral statemen"
+ b"ts in IETF sessi"
+ b"ons, as well as "
+ b"written and elec"
+ b"tronic communica"
+ b"tions made at an"
+ b"y time or place,"
+ b" which are addre"
+ b"ssed to").decode(),
+ "36e5f6b5c5e06070f0efca96227a863e",
+ "RFC7539 A.3 #2",
+ ),
+ (
+ "36e5f6b5c5e06070f0efca96227a863e00000000000000000000000000000000",
+ hexlify(
+ b"Any submission t"
+ b"o the IETF inten"
+ b"ded by the Contr"
+ b"ibutor for publi"
+ b"cation as all or"
+ b" part of an IETF"
+ b" Internet-Draft "
+ b"or RFC and any s"
+ b"tatement made wi"
+ b"thin the context"
+ b" of an IETF acti"
+ b"vity is consider"
+ b"ed an \"IETF Cont"
+ b"ribution\". Such "
+ b"statements inclu"
+ b"de oral statemen"
+ b"ts in IETF sessi"
+ b"ons, as well as "
+ b"written and elec"
+ b"tronic communica"
+ b"tions made at an"
+ b"y time or place,"
+ b" which are addre"
+ b"ssed to").decode(),
+ "f3477e7cd95417af89a6b8794c310cf0",
+ "RFC7539 A.3 #3",
+ ),
+ (
+ "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
+ "2754776173206272696c6c69672c2061"
+ "6e642074686520736c6974687920746f"
+ "7665730a446964206779726520616e64"
+ "2067696d626c6520696e207468652077"
+ "6162653a0a416c6c206d696d73792077"
+ "6572652074686520626f726f676f7665"
+ "732c0a416e6420746865206d6f6d6520"
+ "7261746873206f757467726162652e",
+ "4541669a7eaaee61e708dc7cbcc5eb62",
+ "RFC7539 A.3 #4",
+ ),
+ (
+ "02" + "00" * 31,
+ "FF" * 16,
+ "03" + "00" * 15,
+ "RFC7539 A.3 #5",
+ ),
+ (
+ "02" + "00" * 15 + "FF" * 16,
+ "02" + "00" * 15,
+ "03" + "00" * 15,
+ "RFC7539 A.3 #6",
+ ),
+ (
+ "01" + "00" * 31,
+ "FF" * 16 + "F0" + "FF" * 15 + "11" + "00" * 15,
+ "05" + "00" * 15,
+ "RFC7539 A.3 #7",
+ ),
+ (
+ "01" + "00" * 31,
+ "FF" * 16 + "FB" + "FE" * 15 + "01" * 16,
+ "00" * 16,
+ "RFC7539 A.3 #8",
+ ),
+ (
+ "02" + "00" * 31,
+ "FD" + "FF" * 15,
+ "FA" + "FF" * 15,
+ "RFC7539 A.3 #9",
+ ),
+ (
+ "01 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00"
+ "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "E3 35 94 D7 50 5E 43 B9 00 00 00 00 00 00 00 00"
+ "33 94 D7 50 5E 43 79 CD 01 00 00 00 00 00 00 00"
+ "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
+ "01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "14 00 00 00 00 00 00 00 55 00 00 00 00 00 00 00",
+ "RFC7539 A.3 #10",
+ ),
+ (
+ "01 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00"
+ "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "E3 35 94 D7 50 5E 43 B9 00 00 00 00 00 00 00 00"
+ "33 94 D7 50 5E 43 79 CD 01 00 00 00 00 00 00 00"
+ "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00",
+ "13" + "00" * 15,
+ "RFC7539 A.3 #11",
+ ),
+]
+
+# This is a list of (key(k+r), data, result, description, keywords) tuples.
+test_data_aes = [
+ (
+ "ec074c835580741701425b623235add6851fc40c3467ac0be05cc20404f3f700",
+ "f3f6",
+ "f4c633c3044fc145f84f335cb81953de",
+ "http://cr.yp.to/mac/poly1305-20050329.pdf",
+ { 'cipher':AES, 'nonce':unhexlify("fb447350c4e868c52ac3275cf9d4327e") }
+ ),
+ (
+ "75deaa25c09f208e1dc4ce6b5cad3fbfa0f3080000f46400d0c7e9076c834403",
+ "",
+ "dd3fab2251f11ac759f0887129cc2ee7",
+ "http://cr.yp.to/mac/poly1305-20050329.pdf",
+ { 'cipher':AES, 'nonce':unhexlify("61ee09218d29b0aaed7e154a2c5509cc") }
+ ),
+ (
+ "6acb5f61a7176dd320c5c1eb2edcdc7448443d0bb0d21109c89a100b5ce2c208",
+ "663cea190ffb83d89593f3f476b6bc24"
+ "d7e679107ea26adb8caf6652d0656136",
+ "0ee1c16bb73f0f4fd19881753c01cdbe",
+ "http://cr.yp.to/mac/poly1305-20050329.pdf",
+ { 'cipher':AES, 'nonce':unhexlify("ae212a55399729595dea458bc621ff0e") }
+ ),
+ (
+ "e1a5668a4d5b66a5f68cc5424ed5982d12976a08c4426d0ce8a82407c4f48207",
+ "ab0812724a7f1e342742cbed374d94d1"
+ "36c6b8795d45b3819830f2c04491faf0"
+ "990c62e48b8018b2c3e4a0fa3134cb67"
+ "fa83e158c994d961c4cb21095c1bf9",
+ "5154ad0d2cb26e01274fc51148491f1b",
+ "http://cr.yp.to/mac/poly1305-20050329.pdf",
+ { 'cipher':AES, 'nonce':unhexlify("9ae831e743978d3a23527c7128149e3a") }
+ ),
+]
+
+test_data_chacha20 = [
+ (
+ "00" * 32,
+ "FF" * 15,
+ "13cc5bbadc36b03a5163928f0bcb65aa",
+ "RFC7539 A.4 #1",
+ { 'cipher':ChaCha20, 'nonce':unhexlify("00" * 12) }
+ ),
+ (
+ "00" * 31 + "01",
+ "FF" * 15,
+ "0baf33c1d6df211bdd50a6767e98e00a",
+ "RFC7539 A.4 #2",
+ { 'cipher':ChaCha20, 'nonce':unhexlify("00" * 11 + "02") }
+ ),
+ (
+ "1c 92 40 a5 eb 55 d3 8a f3 33 88 86 04 f6 b5 f0"
+ "47 39 17 c1 40 2b 80 09 9d ca 5c bc 20 70 75 c0",
+ "FF" * 15,
+ "e8b4c6db226cd8939e65e02eebf834ce",
+ "RFC7539 A.4 #3",
+ { 'cipher':ChaCha20, 'nonce':unhexlify("00" * 11 + "02") }
+ ),
+ (
+ "1c 92 40 a5 eb 55 d3 8a f3 33 88 86 04 f6 b5 f0"
+ "47 39 17 c1 40 2b 80 09 9d ca 5c bc 20 70 75 c0",
+ "f3 33 88 86 00 00 00 00 00 00 4e 91 00 00 00 00"
+ "64 a0 86 15 75 86 1a f4 60 f0 62 c7 9b e6 43 bd"
+ "5e 80 5c fd 34 5c f3 89 f1 08 67 0a c7 6c 8c b2"
+ "4c 6c fc 18 75 5d 43 ee a0 9e e9 4e 38 2d 26 b0"
+ "bd b7 b7 3c 32 1b 01 00 d4 f0 3b 7f 35 58 94 cf"
+ "33 2f 83 0e 71 0b 97 ce 98 c8 a8 4a bd 0b 94 81"
+ "14 ad 17 6e 00 8d 33 bd 60 f9 82 b1 ff 37 c8 55"
+ "97 97 a0 6e f4 f0 ef 61 c1 86 32 4e 2b 35 06 38"
+ "36 06 90 7b 6a 7c 02 b0 f9 f6 15 7b 53 c8 67 e4"
+ "b9 16 6c 76 7b 80 4d 46 a5 9b 52 16 cd e7 a4 e9"
+ "90 40 c5 a4 04 33 22 5e e2 82 a1 b0 a0 6c 52 3e"
+ "af 45 34 d7 f8 3f a1 15 5b 00 47 71 8c bc 54 6a"
+ "0d 07 2b 04 b3 56 4e ea 1b 42 22 73 f5 48 27 1a"
+ "0b b2 31 60 53 fa 76 99 19 55 eb d6 31 59 43 4e"
+ "ce bb 4e 46 6d ae 5a 10 73 a6 72 76 27 09 7a 10"
+ "49 e6 17 d9 1d 36 10 94 fa 68 f0 ff 77 98 71 30"
+ "30 5b ea ba 2e da 04 df 99 7b 71 4d 6c 6f 2c 29"
+ "a6 ad 5c b4 02 2b 02 70 9b 00 00 00 00 00 00 00"
+ "0c 00 00 00 00 00 00 00 09 01 00 00 00 00 00 00",
+ "ee ad 9d 67 89 0c bb 22 39 23 36 fe a1 85 1f 38",
+ "RFC7539 A.5",
+ { 'cipher':ChaCha20, 'nonce':unhexlify("000000000102030405060708") }
+ ),
+]
+
+
+class Poly1305Test_AES(unittest.TestCase):
+
+ key = b'\x11' * 32
+
+ def test_new_positive(self):
+
+ data = b'r' * 100
+
+ h1 = Poly1305.new(key=self.key, cipher=AES)
+ self.assertEqual(h1.digest_size, 16)
+ self.assertEqual(len(h1.nonce), 16)
+ d1 = h1.update(data).digest()
+ self.assertEqual(len(d1), 16)
+
+ h2 = Poly1305.new(key=self.key, nonce=h1.nonce, data=data, cipher=AES)
+ d2 = h2.digest()
+ self.assertEqual(h1.nonce, h2.nonce)
+ self.assertEqual(d1, d2)
+
+ def test_new_negative(self):
+ from Crypto.Cipher import DES3
+
+ self.assertRaises(ValueError, Poly1305.new, key=self.key[:31], cipher=AES)
+ self.assertRaises(ValueError, Poly1305.new, key=self.key, cipher=DES3)
+ self.assertRaises(ValueError, Poly1305.new, key=self.key, nonce=b'1' * 15, cipher=AES)
+ self.assertRaises(TypeError, Poly1305.new, key=u"2" * 32, cipher=AES)
+ self.assertRaises(TypeError, Poly1305.new, key=self.key, data=u"2" * 100, cipher=AES)
+
+ def test_update(self):
+ pieces = [b"\x0A" * 200, b"\x14" * 300]
+ h1 = Poly1305.new(key=self.key, cipher=AES)
+ h1.update(pieces[0]).update(pieces[1])
+ d1 = h1.digest()
+
+ h2 = Poly1305.new(key=self.key, cipher=AES, nonce=h1.nonce)
+ h2.update(pieces[0] + pieces[1])
+ d2 = h2.digest()
+ self.assertEqual(d1, d2)
+
+ def test_update_negative(self):
+ h = Poly1305.new(key=self.key, cipher=AES)
+ self.assertRaises(TypeError, h.update, u"string")
+
+ def test_digest(self):
+ h = Poly1305.new(key=self.key, cipher=AES)
+ digest = h.digest()
+
+ # hexdigest does not change the state
+ self.assertEqual(h.digest(), digest)
+ # digest returns a byte string
+ self.assertTrue(isinstance(digest, type(b"digest")))
+
+ def test_update_after_digest(self):
+ msg=b"rrrrttt"
+
+ # Normally, update() cannot be done after digest()
+ h = Poly1305.new(key=self.key, data=msg[:4], cipher=AES)
+ h.digest()
+ self.assertRaises(TypeError, h.update, msg[4:])
+
+ def test_hex_digest(self):
+ mac = Poly1305.new(key=self.key, cipher=AES)
+ digest = mac.digest()
+ hexdigest = mac.hexdigest()
+
+ # hexdigest is equivalent to digest
+ self.assertEqual(hexlify(digest), tobytes(hexdigest))
+ # hexdigest does not change the state
+ self.assertEqual(mac.hexdigest(), hexdigest)
+ # hexdigest returns a string
+ self.assertTrue(isinstance(hexdigest, type("digest")))
+
+ def test_verify(self):
+ h = Poly1305.new(key=self.key, cipher=AES)
+ mac = h.digest()
+ h.verify(mac)
+ wrong_mac = strxor_c(mac, 255)
+ self.assertRaises(ValueError, h.verify, wrong_mac)
+
+ def test_hexverify(self):
+ h = Poly1305.new(key=self.key, cipher=AES)
+ mac = h.hexdigest()
+ h.hexverify(mac)
+ self.assertRaises(ValueError, h.hexverify, "4556")
+
+ def test_bytearray(self):
+
+ data = b"\x00\x01\x02"
+ h0 = Poly1305.new(key=self.key, data=data, cipher=AES)
+ d_ref = h0.digest()
+
+ # Data and key can be a bytearray (during initialization)
+ key_ba = bytearray(self.key)
+ data_ba = bytearray(data)
+
+ h1 = Poly1305.new(key=self.key, data=data, cipher=AES, nonce=h0.nonce)
+ h2 = Poly1305.new(key=key_ba, data=data_ba, cipher=AES, nonce=h0.nonce)
+ key_ba[:1] = b'\xFF'
+ data_ba[:1] = b'\xEE'
+
+ self.assertEqual(h1.digest(), d_ref)
+ self.assertEqual(h2.digest(), d_ref)
+
+ # Data can be a bytearray (during operation)
+ data_ba = bytearray(data)
+
+ h1 = Poly1305.new(key=self.key, cipher=AES)
+ h2 = Poly1305.new(key=self.key, cipher=AES, nonce=h1.nonce)
+ h1.update(data)
+ h2.update(data_ba)
+ data_ba[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+ def test_memoryview(self):
+
+ data = b"\x00\x01\x02"
+
+ def get_mv_ro(data):
+ return memoryview(data)
+
+ def get_mv_rw(data):
+ return memoryview(bytearray(data))
+
+ for get_mv in (get_mv_ro, get_mv_rw):
+
+ # Data and key can be a memoryview (during initialization)
+ key_mv = get_mv(self.key)
+ data_mv = get_mv(data)
+
+ h1 = Poly1305.new(key=self.key, data=data, cipher=AES)
+ h2 = Poly1305.new(key=key_mv, data=data_mv, cipher=AES,
+ nonce=h1.nonce)
+ if not data_mv.readonly:
+ data_mv[:1] = b'\xFF'
+ key_mv[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+ # Data can be a memoryview (during operation)
+ data_mv = get_mv(data)
+
+ h1 = Poly1305.new(key=self.key, cipher=AES)
+ h2 = Poly1305.new(key=self.key, cipher=AES, nonce=h1.nonce)
+ h1.update(data)
+ h2.update(data_mv)
+ if not data_mv.readonly:
+ data_mv[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+
+class Poly1305Test_ChaCha20(unittest.TestCase):
+
+ key = b'\x11' * 32
+
+ def test_new_positive(self):
+ data = b'r' * 100
+
+ h1 = Poly1305.new(key=self.key, cipher=ChaCha20)
+ self.assertEqual(h1.digest_size, 16)
+ self.assertEqual(len(h1.nonce), 12)
+
+ h2 = Poly1305.new(key=self.key, cipher=ChaCha20, nonce = b'8' * 8)
+ self.assertEqual(len(h2.nonce), 8)
+ self.assertEqual(h2.nonce, b'8' * 8)
+
+ def test_new_negative(self):
+
+ self.assertRaises(ValueError, Poly1305.new, key=self.key, nonce=b'1' * 7, cipher=ChaCha20)
+
+
+#
+# make_mac_tests() expect a new() function with signature new(key, data,
+# **kwargs), and we need to adapt Poly1305's, as it only uses keywords
+#
+class Poly1305_New(object):
+
+ @staticmethod
+ def new(key, *data, **kwds):
+ _kwds = dict(kwds)
+ if len(data) == 1:
+ _kwds['data'] = data[0]
+ _kwds['key'] = key
+ return Poly1305.new(**_kwds)
+
+
+class Poly1305_Basic(object):
+
+ @staticmethod
+ def new(key, *data, **kwds):
+ from Crypto.Hash.Poly1305 import Poly1305_MAC
+
+ if len(data) == 1:
+ msg = data[0]
+ else:
+ msg = None
+
+ return Poly1305_MAC(key[:16], key[16:], msg)
+
+
+class Poly1305AES_MC(unittest.TestCase):
+
+ def runTest(self):
+ tag = unhexlify(b"fb447350c4e868c52ac3275cf9d4327e")
+
+ msg = b''
+ for msg_len in range(5000 + 1):
+ key = tag + strxor_c(tag, 0xFF)
+ nonce = tag[::-1]
+ if msg_len > 0:
+ msg = msg + tobytes(tag[0])
+ auth = Poly1305.new(key=key, nonce=nonce, cipher=AES, data=msg)
+ tag = auth.digest()
+
+ # Compare against output of original DJB's poly1305aes-20050218
+ self.assertEqual("CDFA436DDD629C7DC20E1128530BAED2", auth.hexdigest().upper())
+
+
+def get_tests(config={}):
+ tests = make_mac_tests(Poly1305_Basic, "Poly1305", test_data_basic)
+ tests += make_mac_tests(Poly1305_New, "Poly1305", test_data_aes)
+ tests += make_mac_tests(Poly1305_New, "Poly1305", test_data_chacha20)
+ tests += [ Poly1305AES_MC() ]
+ tests += list_test_cases(Poly1305Test_AES)
+ tests += list_test_cases(Poly1305Test_ChaCha20)
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_RIPEMD160.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_RIPEMD160.py
new file mode 100644
index 00000000..153c5700
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_RIPEMD160.py
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/test_RIPEMD160.py: Self-test for the RIPEMD-160 hash function
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+#"""Self-test suite for Crypto.Hash.RIPEMD160"""
+
+from Crypto.Util.py3compat import *
+
+# This is a list of (expected_result, input[, description]) tuples.
+test_data = [
+ # Test vectors downloaded 2008-09-12 from
+ # http://homes.esat.kuleuven.be/~bosselae/ripemd160.html
+ ('9c1185a5c5e9fc54612808977ee8f548b2258d31', '', "'' (empty string)"),
+ ('0bdc9d2d256b3ee9daae347be6f4dc835a467ffe', 'a'),
+ ('8eb208f7e05d987a9b044a8e98c6b087f15a0bfc', 'abc'),
+ ('5d0689ef49d2fae572b881b123a85ffa21595f36', 'message digest'),
+
+ ('f71c27109c692c1b56bbdceb5b9d2865b3708dbc',
+ 'abcdefghijklmnopqrstuvwxyz',
+ 'a-z'),
+
+ ('12a053384a9c0c88e405a06c27dcf49ada62eb2b',
+ 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq',
+ 'abcdbcd...pnopq'),
+
+ ('b0e20b6e3116640286ed3a87a5713079b21f5189',
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
+ 'A-Z, a-z, 0-9'),
+
+ ('9b752e45573d4b39f4dbd3323cab82bf63326bfb',
+ '1234567890' * 8,
+ "'1234567890' * 8"),
+
+ ('52783243c1697bdbe16d37f97f68f08325dc1528',
+ 'a' * 10**6,
+ '"a" * 10**6'),
+]
+
+def get_tests(config={}):
+ from Crypto.Hash import RIPEMD160
+ from .common import make_hash_tests
+ return make_hash_tests(RIPEMD160, "RIPEMD160", test_data,
+ digest_size=20,
+ oid="1.3.36.3.2.1")
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA1.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA1.py
new file mode 100644
index 00000000..a883a44b
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA1.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/SHA1.py: Self-test for the SHA-1 hash function
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.SHA"""
+
+from binascii import hexlify
+
+from Crypto.SelfTest.loader import load_test_vectors
+
+# Test vectors from various sources
+# This is a list of (expected_result, input[, description]) tuples.
+test_data_various = [
+ # FIPS PUB 180-2, A.1 - "One-Block Message"
+ ('a9993e364706816aba3e25717850c26c9cd0d89d', 'abc'),
+
+ # FIPS PUB 180-2, A.2 - "Multi-Block Message"
+ ('84983e441c3bd26ebaae4aa1f95129e5e54670f1',
+ 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'),
+
+ # FIPS PUB 180-2, A.3 - "Long Message"
+# ('34aa973cd4c4daa4f61eeb2bdbad27316534016f',
+# 'a' * 10**6,
+# '"a" * 10**6'),
+
+ # RFC 3174: Section 7.3, "TEST4" (multiple of 512 bits)
+ ('dea356a2cddd90c7a7ecedc5ebb563934f460452',
+ '01234567' * 80,
+ '"01234567" * 80'),
+]
+
+def get_tests(config={}):
+ from Crypto.Hash import SHA1
+ from .common import make_hash_tests
+
+ tests = []
+
+ test_vectors = load_test_vectors(("Hash", "SHA1"),
+ "SHA1ShortMsg.rsp",
+ "KAT SHA-1",
+ { "len" : lambda x: int(x) } ) or []
+
+ test_data = test_data_various[:]
+ for tv in test_vectors:
+ try:
+ if tv.startswith('['):
+ continue
+ except AttributeError:
+ pass
+ if tv.len == 0:
+ tv.msg = b""
+ test_data.append((hexlify(tv.md), tv.msg, tv.desc))
+
+ tests = make_hash_tests(SHA1, "SHA1", test_data,
+ digest_size=20,
+ oid="1.3.14.3.2.26")
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA224.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA224.py
new file mode 100644
index 00000000..cf81ad98
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA224.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/test_SHA224.py: Self-test for the SHA-224 hash function
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.SHA224"""
+
+# Test vectors from various sources
+# This is a list of (expected_result, input[, description]) tuples.
+test_data = [
+
+ # RFC 3874: Section 3.1, "Test Vector #1
+ ('23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7', 'abc'),
+
+ # RFC 3874: Section 3.2, "Test Vector #2
+ ('75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525', 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'),
+
+ # RFC 3874: Section 3.3, "Test Vector #3
+ ('20794655980c91d8bbb4c1ea97618a4bf03f42581948b2ee4ee7ad67', 'a' * 10**6, "'a' * 10**6"),
+
+ # Examples from http://de.wikipedia.org/wiki/Secure_Hash_Algorithm
+ ('d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f', ''),
+
+ ('49b08defa65e644cbf8a2dd9270bdededabc741997d1dadd42026d7b',
+ 'Franz jagt im komplett verwahrlosten Taxi quer durch Bayern'),
+
+ ('58911e7fccf2971a7d07f93162d8bd13568e71aa8fc86fc1fe9043d1',
+ 'Frank jagt im komplett verwahrlosten Taxi quer durch Bayern'),
+
+]
+
+def get_tests(config={}):
+ from Crypto.Hash import SHA224
+ from .common import make_hash_tests
+ return make_hash_tests(SHA224, "SHA224", test_data,
+ digest_size=28,
+ oid='2.16.840.1.101.3.4.2.4')
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA256.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA256.py
new file mode 100644
index 00000000..bb99326d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA256.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/test_SHA256.py: Self-test for the SHA-256 hash function
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.SHA256"""
+
+import unittest
+from Crypto.Util.py3compat import *
+
+class LargeSHA256Test(unittest.TestCase):
+ def runTest(self):
+ """SHA256: 512/520 MiB test"""
+ from Crypto.Hash import SHA256
+ zeros = bchr(0x00) * (1024*1024)
+
+ h = SHA256.new(zeros)
+ for i in range(511):
+ h.update(zeros)
+
+ # This test vector is from PyCrypto's old testdata.py file.
+ self.assertEqual('9acca8e8c22201155389f65abbf6bc9723edc7384ead80503839f49dcc56d767', h.hexdigest()) # 512 MiB
+
+ for i in range(8):
+ h.update(zeros)
+
+ # This test vector is from PyCrypto's old testdata.py file.
+ self.assertEqual('abf51ad954b246009dfe5a50ecd582fd5b8f1b8b27f30393853c3ef721e7fa6e', h.hexdigest()) # 520 MiB
+
+def get_tests(config={}):
+ # Test vectors from FIPS PUB 180-2
+ # This is a list of (expected_result, input[, description]) tuples.
+ test_data = [
+ # FIPS PUB 180-2, B.1 - "One-Block Message"
+ ('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
+ 'abc'),
+
+ # FIPS PUB 180-2, B.2 - "Multi-Block Message"
+ ('248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1',
+ 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'),
+
+ # FIPS PUB 180-2, B.3 - "Long Message"
+ ('cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0',
+ 'a' * 10**6,
+ '"a" * 10**6'),
+
+ # Test for an old PyCrypto bug.
+ ('f7fd017a3c721ce7ff03f3552c0813adcc48b7f33f07e5e2ba71e23ea393d103',
+ 'This message is precisely 55 bytes long, to test a bug.',
+ 'Length = 55 (mod 64)'),
+
+ # Example from http://de.wikipedia.org/wiki/Secure_Hash_Algorithm
+ ('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', ''),
+
+ ('d32b568cd1b96d459e7291ebf4b25d007f275c9f13149beeb782fac0716613f8',
+ 'Franz jagt im komplett verwahrlosten Taxi quer durch Bayern'),
+ ]
+
+ from Crypto.Hash import SHA256
+ from .common import make_hash_tests
+ tests = make_hash_tests(SHA256, "SHA256", test_data,
+ digest_size=32,
+ oid="2.16.840.1.101.3.4.2.1")
+
+ if config.get('slow_tests'):
+ tests += [LargeSHA256Test()]
+
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA384.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA384.py
new file mode 100644
index 00000000..c682eb43
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA384.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/test_SHA.py: Self-test for the SHA-384 hash function
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.SHA384"""
+
+# Test vectors from various sources
+# This is a list of (expected_result, input[, description]) tuples.
+test_data = [
+
+ # RFC 4634: Section Page 8.4, "Test 1"
+ ('cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7', 'abc'),
+
+ # RFC 4634: Section Page 8.4, "Test 2.2"
+ ('09330c33f71147e83d192fc782cd1b4753111b173b3b05d22fa08086e3b0f712fcc7c71a557e2db966c3e9fa91746039', 'abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu'),
+
+ # RFC 4634: Section Page 8.4, "Test 3"
+ ('9d0e1809716474cb086e834e310a4a1ced149e9c00f248527972cec5704c2a5b07b8b3dc38ecc4ebae97ddd87f3d8985', 'a' * 10**6, "'a' * 10**6"),
+
+ # Taken from http://de.wikipedia.org/wiki/Secure_Hash_Algorithm
+ ('38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b', ''),
+
+ # Example from http://de.wikipedia.org/wiki/Secure_Hash_Algorithm
+ ('71e8383a4cea32d6fd6877495db2ee353542f46fa44bc23100bca48f3366b84e809f0708e81041f427c6d5219a286677',
+ 'Franz jagt im komplett verwahrlosten Taxi quer durch Bayern'),
+
+]
+
+def get_tests(config={}):
+ from Crypto.Hash import SHA384
+ from .common import make_hash_tests
+ return make_hash_tests(SHA384, "SHA384", test_data,
+ digest_size=48,
+ oid='2.16.840.1.101.3.4.2.2')
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_224.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_224.py
new file mode 100644
index 00000000..f92147a7
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_224.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/test_SHA3_224.py: Self-test for the SHA-3/224 hash function
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.SHA3_224"""
+
+import unittest
+from binascii import hexlify
+
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Hash import SHA3_224 as SHA3
+from Crypto.Util.py3compat import b
+
+
+class APITest(unittest.TestCase):
+
+ def test_update_after_digest(self):
+ msg=b("rrrrttt")
+
+ # Normally, update() cannot be done after digest()
+ h = SHA3.new(data=msg[:4])
+ dig1 = h.digest()
+ self.assertRaises(TypeError, h.update, msg[4:])
+ dig2 = SHA3.new(data=msg).digest()
+
+ # With the proper flag, it is allowed
+ h = SHA3.new(data=msg[:4], update_after_digest=True)
+ self.assertEqual(h.digest(), dig1)
+ # ... and the subsequent digest applies to the entire message
+ # up to that point
+ h.update(msg[4:])
+ self.assertEqual(h.digest(), dig2)
+
+
+def get_tests(config={}):
+ from .common import make_hash_tests
+
+ tests = []
+
+ test_vectors = load_test_vectors(("Hash", "SHA3"),
+ "ShortMsgKAT_SHA3-224.txt",
+ "KAT SHA-3 224",
+ { "len" : lambda x: int(x) } ) or []
+
+ test_data = []
+ for tv in test_vectors:
+ if tv.len == 0:
+ tv.msg = b("")
+ test_data.append((hexlify(tv.md), tv.msg, tv.desc))
+
+ tests += make_hash_tests(SHA3, "SHA3_224", test_data,
+ digest_size=SHA3.digest_size,
+ oid="2.16.840.1.101.3.4.2.7")
+ tests += list_test_cases(APITest)
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_256.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_256.py
new file mode 100644
index 00000000..432c9321
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_256.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/test_SHA3_256.py: Self-test for the SHA-3/256 hash function
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.SHA3_256"""
+
+import unittest
+from binascii import hexlify
+
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Hash import SHA3_256 as SHA3
+from Crypto.Util.py3compat import b
+
+
+class APITest(unittest.TestCase):
+
+ def test_update_after_digest(self):
+ msg=b("rrrrttt")
+
+ # Normally, update() cannot be done after digest()
+ h = SHA3.new(data=msg[:4])
+ dig1 = h.digest()
+ self.assertRaises(TypeError, h.update, msg[4:])
+ dig2 = SHA3.new(data=msg).digest()
+
+ # With the proper flag, it is allowed
+ h = SHA3.new(data=msg[:4], update_after_digest=True)
+ self.assertEqual(h.digest(), dig1)
+ # ... and the subsequent digest applies to the entire message
+ # up to that point
+ h.update(msg[4:])
+ self.assertEqual(h.digest(), dig2)
+
+
+def get_tests(config={}):
+ from .common import make_hash_tests
+
+ tests = []
+
+ test_vectors = load_test_vectors(("Hash", "SHA3"),
+ "ShortMsgKAT_SHA3-256.txt",
+ "KAT SHA-3 256",
+ { "len" : lambda x: int(x) } ) or []
+
+ test_data = []
+ for tv in test_vectors:
+ if tv.len == 0:
+ tv.msg = b("")
+ test_data.append((hexlify(tv.md), tv.msg, tv.desc))
+
+
+ tests += make_hash_tests(SHA3, "SHA3_256", test_data,
+ digest_size=SHA3.digest_size,
+ oid="2.16.840.1.101.3.4.2.8")
+ tests += list_test_cases(APITest)
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_384.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_384.py
new file mode 100644
index 00000000..b0ba1bfe
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_384.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/test_SHA3_384.py: Self-test for the SHA-3/384 hash function
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.SHA3_384"""
+
+import unittest
+from binascii import hexlify
+
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Hash import SHA3_384 as SHA3
+from Crypto.Util.py3compat import b
+
+
+class APITest(unittest.TestCase):
+
+ def test_update_after_digest(self):
+ msg=b("rrrrttt")
+
+ # Normally, update() cannot be done after digest()
+ h = SHA3.new(data=msg[:4])
+ dig1 = h.digest()
+ self.assertRaises(TypeError, h.update, msg[4:])
+ dig2 = SHA3.new(data=msg).digest()
+
+ # With the proper flag, it is allowed
+ h = SHA3.new(data=msg[:4], update_after_digest=True)
+ self.assertEqual(h.digest(), dig1)
+ # ... and the subsequent digest applies to the entire message
+ # up to that point
+ h.update(msg[4:])
+ self.assertEqual(h.digest(), dig2)
+
+
+def get_tests(config={}):
+ from .common import make_hash_tests
+
+ tests = []
+
+ test_vectors = load_test_vectors(("Hash", "SHA3"),
+ "ShortMsgKAT_SHA3-384.txt",
+ "KAT SHA-3 384",
+ { "len" : lambda x: int(x) } ) or []
+
+ test_data = []
+ for tv in test_vectors:
+ if tv.len == 0:
+ tv.msg = b("")
+ test_data.append((hexlify(tv.md), tv.msg, tv.desc))
+
+ tests += make_hash_tests(SHA3, "SHA3_384", test_data,
+ digest_size=SHA3.digest_size,
+ oid="2.16.840.1.101.3.4.2.9")
+ tests += list_test_cases(APITest)
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_512.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_512.py
new file mode 100644
index 00000000..7d1007a6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA3_512.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/test_SHA3_512.py: Self-test for the SHA-3/512 hash function
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.SHA3_512"""
+
+import unittest
+from binascii import hexlify
+
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Hash import SHA3_512 as SHA3
+from Crypto.Util.py3compat import b
+
+
+class APITest(unittest.TestCase):
+
+ def test_update_after_digest(self):
+ msg=b("rrrrttt")
+
+ # Normally, update() cannot be done after digest()
+ h = SHA3.new(data=msg[:4])
+ dig1 = h.digest()
+ self.assertRaises(TypeError, h.update, msg[4:])
+ dig2 = SHA3.new(data=msg).digest()
+
+ # With the proper flag, it is allowed
+ h = SHA3.new(data=msg[:4], update_after_digest=True)
+ self.assertEqual(h.digest(), dig1)
+ # ... and the subsequent digest applies to the entire message
+ # up to that point
+ h.update(msg[4:])
+ self.assertEqual(h.digest(), dig2)
+
+
+def get_tests(config={}):
+ from .common import make_hash_tests
+
+ tests = []
+
+ test_vectors = load_test_vectors(("Hash", "SHA3"),
+ "ShortMsgKAT_SHA3-512.txt",
+ "KAT SHA-3 512",
+ { "len" : lambda x: int(x) } ) or []
+
+ test_data = []
+ for tv in test_vectors:
+ if tv.len == 0:
+ tv.msg = b("")
+ test_data.append((hexlify(tv.md), tv.msg, tv.desc))
+
+ tests += make_hash_tests(SHA3, "SHA3_512", test_data,
+ digest_size=SHA3.digest_size,
+ oid="2.16.840.1.101.3.4.2.10")
+ tests += list_test_cases(APITest)
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA512.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA512.py
new file mode 100644
index 00000000..20961aca
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHA512.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Hash/test_SHA512.py: Self-test for the SHA-512 hash function
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.SHA512"""
+
+from binascii import hexlify
+
+from Crypto.Hash import SHA512
+from .common import make_hash_tests
+from Crypto.SelfTest.loader import load_test_vectors
+
+# Test vectors from various sources
+# This is a list of (expected_result, input[, description]) tuples.
+test_data_512_other = [
+
+ # RFC 4634: Section Page 8.4, "Test 1"
+ ('ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f', 'abc'),
+
+ # RFC 4634: Section Page 8.4, "Test 2.1"
+ ('8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909', 'abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu'),
+
+ # RFC 4634: Section Page 8.4, "Test 3"
+ ('e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b', 'a' * 10**6, "'a' * 10**6"),
+
+ # Taken from http://de.wikipedia.org/wiki/Secure_Hash_Algorithm
+ ('cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e', ''),
+
+ ('af9ed2de700433b803240a552b41b5a472a6ef3fe1431a722b2063c75e9f07451f67a28e37d09cde769424c96aea6f8971389db9e1993d6c565c3c71b855723c', 'Franz jagt im komplett verwahrlosten Taxi quer durch Bayern'),
+]
+
+
+def get_tests_SHA512():
+
+ test_vectors = load_test_vectors(("Hash", "SHA2"),
+ "SHA512ShortMsg.rsp",
+ "KAT SHA-512",
+ {"len": lambda x: int(x)}) or []
+
+ test_data = test_data_512_other[:]
+ for tv in test_vectors:
+ try:
+ if tv.startswith('['):
+ continue
+ except AttributeError:
+ pass
+ if tv.len == 0:
+ tv.msg = b""
+ test_data.append((hexlify(tv.md), tv.msg, tv.desc))
+
+ tests = make_hash_tests(SHA512, "SHA512", test_data,
+ digest_size=64,
+ oid="2.16.840.1.101.3.4.2.3")
+ return tests
+
+
+def get_tests_SHA512_224():
+
+ test_vectors = load_test_vectors(("Hash", "SHA2"),
+ "SHA512_224ShortMsg.rsp",
+ "KAT SHA-512/224",
+ {"len": lambda x: int(x)}) or []
+
+ test_data = []
+ for tv in test_vectors:
+ try:
+ if tv.startswith('['):
+ continue
+ except AttributeError:
+ pass
+ if tv.len == 0:
+ tv.msg = b""
+ test_data.append((hexlify(tv.md), tv.msg, tv.desc))
+
+ tests = make_hash_tests(SHA512, "SHA512/224", test_data,
+ digest_size=28,
+ oid="2.16.840.1.101.3.4.2.5",
+ extra_params={ "truncate" : "224" })
+ return tests
+
+
+def get_tests_SHA512_256():
+
+ test_vectors = load_test_vectors(("Hash", "SHA2"),
+ "SHA512_256ShortMsg.rsp",
+ "KAT SHA-512/256",
+ {"len": lambda x: int(x)}) or []
+
+ test_data = []
+ for tv in test_vectors:
+ try:
+ if tv.startswith('['):
+ continue
+ except AttributeError:
+ pass
+ if tv.len == 0:
+ tv.msg = b""
+ test_data.append((hexlify(tv.md), tv.msg, tv.desc))
+
+ tests = make_hash_tests(SHA512, "SHA512/256", test_data,
+ digest_size=32,
+ oid="2.16.840.1.101.3.4.2.6",
+ extra_params={ "truncate" : "256" })
+ return tests
+
+
+def get_tests(config={}):
+
+ tests = []
+ tests += get_tests_SHA512()
+ tests += get_tests_SHA512_224()
+ tests += get_tests_SHA512_256()
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHAKE.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHAKE.py
new file mode 100644
index 00000000..d8423b87
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_SHAKE.py
@@ -0,0 +1,151 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.SHAKE128 and SHAKE256"""
+
+import unittest
+from binascii import hexlify, unhexlify
+
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Hash import SHAKE128, SHAKE256
+from Crypto.Util.py3compat import b, bchr, bord, tobytes
+
+class SHAKETest(unittest.TestCase):
+
+ def test_new_positive(self):
+
+ xof1 = self.shake.new()
+ xof2 = self.shake.new(data=b("90"))
+ xof3 = self.shake.new().update(b("90"))
+
+ self.assertNotEqual(xof1.read(10), xof2.read(10))
+ xof3.read(10)
+ self.assertEqual(xof2.read(10), xof3.read(10))
+
+ def test_update(self):
+ pieces = [bchr(10) * 200, bchr(20) * 300]
+ h = self.shake.new()
+ h.update(pieces[0]).update(pieces[1])
+ digest = h.read(10)
+ h = self.shake.new()
+ h.update(pieces[0] + pieces[1])
+ self.assertEqual(h.read(10), digest)
+
+ def test_update_negative(self):
+ h = self.shake.new()
+ self.assertRaises(TypeError, h.update, u"string")
+
+ def test_digest(self):
+ h = self.shake.new()
+ digest = h.read(90)
+
+ # read returns a byte string of the right length
+ self.assertTrue(isinstance(digest, type(b("digest"))))
+ self.assertEqual(len(digest), 90)
+
+ def test_update_after_read(self):
+ mac = self.shake.new()
+ mac.update(b("rrrr"))
+ mac.read(90)
+ self.assertRaises(TypeError, mac.update, b("ttt"))
+
+ def test_copy(self):
+ mac = self.shake.new()
+ mac.update(b("rrrr"))
+ mac2 = mac.copy()
+ x1 = mac.read(90)
+ x2 = mac2.read(90)
+ self.assertEqual(x1, x2)
+
+
+class SHAKE128Test(SHAKETest):
+ shake = SHAKE128
+
+
+class SHAKE256Test(SHAKETest):
+ shake = SHAKE256
+
+
+class SHAKEVectors(unittest.TestCase):
+ pass
+
+
+test_vectors_128 = load_test_vectors(("Hash", "SHA3"),
+ "ShortMsgKAT_SHAKE128.txt",
+ "Short Messages KAT SHAKE128",
+ { "len" : lambda x: int(x) } ) or []
+
+for idx, tv in enumerate(test_vectors_128):
+ if tv.len == 0:
+ data = b("")
+ else:
+ data = tobytes(tv.msg)
+
+ def new_test(self, data=data, result=tv.md):
+ hobj = SHAKE128.new(data=data)
+ digest = hobj.read(len(result))
+ self.assertEqual(digest, result)
+
+ setattr(SHAKEVectors, "test_128_%d" % idx, new_test)
+
+
+test_vectors_256 = load_test_vectors(("Hash", "SHA3"),
+ "ShortMsgKAT_SHAKE256.txt",
+ "Short Messages KAT SHAKE256",
+ { "len" : lambda x: int(x) } ) or []
+
+for idx, tv in enumerate(test_vectors_256):
+ if tv.len == 0:
+ data = b("")
+ else:
+ data = tobytes(tv.msg)
+
+ def new_test(self, data=data, result=tv.md):
+ hobj = SHAKE256.new(data=data)
+ digest = hobj.read(len(result))
+ self.assertEqual(digest, result)
+
+ setattr(SHAKEVectors, "test_256_%d" % idx, new_test)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(SHAKE128Test)
+ tests += list_test_cases(SHAKE256Test)
+ tests += list_test_cases(SHAKEVectors)
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_TupleHash.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_TupleHash.py
new file mode 100644
index 00000000..f3fc3d2f
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_TupleHash.py
@@ -0,0 +1,302 @@
+import unittest
+from binascii import unhexlify, hexlify
+
+from Crypto.Util.py3compat import tobytes
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Hash import TupleHash128, TupleHash256
+
+
+class TupleHashTest(unittest.TestCase):
+
+ def new(self, *args, **kwargs):
+ return self.TupleHash.new(*args, **kwargs)
+
+ def test_new_positive(self):
+
+ h = self.new()
+ for new_func in self.TupleHash.new, h.new:
+
+ for dbits in range(64, 1024 + 1, 8):
+ hobj = new_func(digest_bits=dbits)
+ self.assertEqual(hobj.digest_size * 8, dbits)
+
+ for dbytes in range(8, 128 + 1):
+ hobj = new_func(digest_bytes=dbytes)
+ self.assertEqual(hobj.digest_size, dbytes)
+
+ hobj = h.new()
+ self.assertEqual(hobj.digest_size, self.default_bytes)
+
+ def test_new_negative(self):
+
+ h = self.new()
+ for new_func in self.TupleHash.new, h.new:
+ self.assertRaises(TypeError, new_func,
+ digest_bytes=self.minimum_bytes,
+ digest_bits=self.minimum_bits)
+ self.assertRaises(ValueError, new_func, digest_bytes=0)
+ self.assertRaises(ValueError, new_func,
+ digest_bits=self.minimum_bits + 7)
+ self.assertRaises(ValueError, new_func,
+ digest_bits=self.minimum_bits - 8)
+ self.assertRaises(ValueError, new_func,
+ digest_bits=self.minimum_bytes - 1)
+
+ def test_default_digest_size(self):
+ digest = self.new().digest()
+ self.assertEqual(len(digest), self.default_bytes)
+
+ def test_update(self):
+ h = self.new()
+ h.update(b'')
+ h.digest()
+
+ h = self.new()
+ h.update(b'')
+ h.update(b'STRING1')
+ h.update(b'STRING2')
+ mac1 = h.digest()
+
+ h = self.new()
+ h.update(b'STRING1')
+ h.update(b'STRING2')
+ mac2 = h.digest()
+ self.assertNotEqual(mac1, mac2)
+
+ h = self.new()
+ h.update(b'STRING1', b'STRING2')
+ self.assertEqual(mac2, h.digest())
+
+ h = self.new()
+ t = b'STRING1', b'STRING2'
+ h.update(*t)
+ self.assertEqual(mac2, h.digest())
+
+ def test_update_negative(self):
+ h = self.new()
+ self.assertRaises(TypeError, h.update, u"string")
+ self.assertRaises(TypeError, h.update, None)
+ self.assertRaises(TypeError, h.update, (b'STRING1', b'STRING2'))
+
+ def test_digest(self):
+ h = self.new()
+ digest = h.digest()
+
+ # hexdigest does not change the state
+ self.assertEqual(h.digest(), digest)
+ # digest returns a byte string
+ self.assertTrue(isinstance(digest, type(b"digest")))
+
+ def test_update_after_digest(self):
+ msg = b"rrrrttt"
+
+ # Normally, update() cannot be done after digest()
+ h = self.new()
+ h.update(msg)
+ dig1 = h.digest()
+ self.assertRaises(TypeError, h.update, dig1)
+
+ def test_hex_digest(self):
+ mac = self.new()
+ digest = mac.digest()
+ hexdigest = mac.hexdigest()
+
+ # hexdigest is equivalent to digest
+ self.assertEqual(hexlify(digest), tobytes(hexdigest))
+ # hexdigest does not change the state
+ self.assertEqual(mac.hexdigest(), hexdigest)
+ # hexdigest returns a string
+ self.assertTrue(isinstance(hexdigest, type("digest")))
+
+ def test_bytearray(self):
+
+ data = b"\x00\x01\x02"
+
+ # Data can be a bytearray (during operation)
+ data_ba = bytearray(data)
+
+ h1 = self.new()
+ h2 = self.new()
+ h1.update(data)
+ h2.update(data_ba)
+ data_ba[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+ def test_memoryview(self):
+
+ data = b"\x00\x01\x02"
+
+ def get_mv_ro(data):
+ return memoryview(data)
+
+ def get_mv_rw(data):
+ return memoryview(bytearray(data))
+
+ for get_mv in (get_mv_ro, get_mv_rw):
+
+ # Data can be a memoryview (during operation)
+ data_mv = get_mv(data)
+
+ h1 = self.new()
+ h2 = self.new()
+ h1.update(data)
+ h2.update(data_mv)
+ if not data_mv.readonly:
+ data_mv[:1] = b'\xFF'
+
+ self.assertEqual(h1.digest(), h2.digest())
+
+
+class TupleHash128Test(TupleHashTest):
+
+ TupleHash = TupleHash128
+
+ minimum_bytes = 8
+ default_bytes = 64
+
+ minimum_bits = 64
+ default_bits = 512
+
+
+class TupleHash256Test(TupleHashTest):
+
+ TupleHash = TupleHash256
+
+ minimum_bytes = 8
+ default_bytes = 64
+
+ minimum_bits = 64
+ default_bits = 512
+
+
+class NISTExampleTestVectors(unittest.TestCase):
+
+ # http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/TupleHash_samples.pdf
+ test_data = [
+ (
+ (
+ "00 01 02",
+ "10 11 12 13 14 15",
+ ),
+ "",
+ "C5 D8 78 6C 1A FB 9B 82 11 1A B3 4B 65 B2 C0 04"
+ "8F A6 4E 6D 48 E2 63 26 4C E1 70 7D 3F FC 8E D1",
+ "KMAC128 Sample #1 NIST",
+ TupleHash128
+ ),
+ (
+ (
+ "00 01 02",
+ "10 11 12 13 14 15",
+ ),
+ "My Tuple App",
+ "75 CD B2 0F F4 DB 11 54 E8 41 D7 58 E2 41 60 C5"
+ "4B AE 86 EB 8C 13 E7 F5 F4 0E B3 55 88 E9 6D FB",
+ "KMAC128 Sample #2 NIST",
+ TupleHash128
+ ),
+ (
+ (
+ "00 01 02",
+ "10 11 12 13 14 15",
+ "20 21 22 23 24 25 26 27 28",
+ ),
+ "My Tuple App",
+ "E6 0F 20 2C 89 A2 63 1E DA 8D 4C 58 8C A5 FD 07"
+ "F3 9E 51 51 99 8D EC CF 97 3A DB 38 04 BB 6E 84",
+ "KMAC128 Sample #3 NIST",
+ TupleHash128
+ ),
+ (
+ (
+ "00 01 02",
+ "10 11 12 13 14 15",
+ ),
+ "",
+ "CF B7 05 8C AC A5 E6 68 F8 1A 12 A2 0A 21 95 CE"
+ "97 A9 25 F1 DB A3 E7 44 9A 56 F8 22 01 EC 60 73"
+ "11 AC 26 96 B1 AB 5E A2 35 2D F1 42 3B DE 7B D4"
+ "BB 78 C9 AE D1 A8 53 C7 86 72 F9 EB 23 BB E1 94",
+ "KMAC256 Sample #4 NIST",
+ TupleHash256
+ ),
+ (
+ (
+ "00 01 02",
+ "10 11 12 13 14 15",
+ ),
+ "My Tuple App",
+ "14 7C 21 91 D5 ED 7E FD 98 DB D9 6D 7A B5 A1 16"
+ "92 57 6F 5F E2 A5 06 5F 3E 33 DE 6B BA 9F 3A A1"
+ "C4 E9 A0 68 A2 89 C6 1C 95 AA B3 0A EE 1E 41 0B"
+ "0B 60 7D E3 62 0E 24 A4 E3 BF 98 52 A1 D4 36 7E",
+ "KMAC256 Sample #5 NIST",
+ TupleHash256
+ ),
+ (
+ (
+ "00 01 02",
+ "10 11 12 13 14 15",
+ "20 21 22 23 24 25 26 27 28",
+ ),
+ "My Tuple App",
+ "45 00 0B E6 3F 9B 6B FD 89 F5 47 17 67 0F 69 A9"
+ "BC 76 35 91 A4 F0 5C 50 D6 88 91 A7 44 BC C6 E7"
+ "D6 D5 B5 E8 2C 01 8D A9 99 ED 35 B0 BB 49 C9 67"
+ "8E 52 6A BD 8E 85 C1 3E D2 54 02 1D B9 E7 90 CE",
+ "KMAC256 Sample #6 NIST",
+ TupleHash256
+ ),
+
+
+
+ ]
+
+ def setUp(self):
+ td = []
+ for tv_in in self.test_data:
+ tv_out = [None] * len(tv_in)
+
+ tv_out[0] = []
+ for string in tv_in[0]:
+ tv_out[0].append(unhexlify(string.replace(" ", "")))
+
+ tv_out[1] = tobytes(tv_in[1]) # Custom
+ tv_out[2] = unhexlify(tv_in[2].replace(" ", ""))
+ tv_out[3] = tv_in[3]
+ tv_out[4] = tv_in[4]
+ td.append(tv_out)
+ self.test_data = td
+
+ def runTest(self):
+
+ for data, custom, digest, text, module in self.test_data:
+ hd1 = module.new(custom=custom, digest_bytes=len(digest))
+ hd2 = module.new(custom=custom, digest_bytes=len(digest))
+
+ # Call update() for each element
+ for string in data:
+ hd1.update(string)
+
+ # One single update for all elements
+ hd2.update(*data)
+
+ self.assertEqual(hd1.digest(), digest, msg=text)
+ self.assertEqual(hd2.digest(), digest, msg=text)
+
+def get_tests(config={}):
+ tests = []
+
+ tests += list_test_cases(TupleHash128Test)
+ tests += list_test_cases(TupleHash256Test)
+ tests.append(NISTExampleTestVectors())
+
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_TurboSHAKE.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_TurboSHAKE.py
new file mode 100644
index 00000000..b37aee1a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_TurboSHAKE.py
@@ -0,0 +1,468 @@
+"""Self-test suite for Crypto.Hash.TurboSHAKE128 and TurboSHAKE256"""
+
+import unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Hash import TurboSHAKE128, TurboSHAKE256
+from Crypto.Util.py3compat import bchr
+
+
+class TurboSHAKETest(unittest.TestCase):
+
+ def test_new_positive(self):
+
+ xof1 = self.TurboSHAKE.new()
+ xof1.update(b'90')
+
+ xof2 = self.TurboSHAKE.new(domain=0x1F)
+ xof2.update(b'90')
+
+ xof3 = self.TurboSHAKE.new(data=b'90')
+
+ out1 = xof1.read(128)
+ out2 = xof2.read(128)
+ out3 = xof3.read(128)
+
+ self.assertEqual(out1, out2)
+ self.assertEqual(out1, out3)
+
+ def test_new_domain(self):
+ xof1 = self.TurboSHAKE.new(domain=0x1D)
+ xof2 = self.TurboSHAKE.new(domain=0x20)
+ self.assertNotEqual(xof1.read(128), xof2.read(128))
+
+ def test_update(self):
+ pieces = [bchr(10) * 200, bchr(20) * 300]
+
+ xof1 = self.TurboSHAKE.new()
+ xof1.update(pieces[0]).update(pieces[1])
+ digest1 = xof1.read(10)
+
+ xof2 = self.TurboSHAKE.new()
+ xof2.update(pieces[0] + pieces[1])
+ digest2 = xof2.read(10)
+
+ self.assertEqual(digest1, digest2)
+
+ def test_update_negative(self):
+ xof1 = self.TurboSHAKE.new()
+ self.assertRaises(TypeError, xof1.update, u"string")
+
+ def test_read(self):
+ xof1 = self.TurboSHAKE.new()
+ digest = xof1.read(90)
+
+ # read returns a byte string of the right length
+ self.assertTrue(isinstance(digest, bytes))
+ self.assertEqual(len(digest), 90)
+
+ def test_update_after_read(self):
+ xof1 = self.TurboSHAKE.new()
+ xof1.update(b"rrrr")
+ xof1.read(90)
+ self.assertRaises(TypeError, xof1.update, b"ttt")
+
+ def test_new(self):
+ xof1 = self.TurboSHAKE.new(domain=0x07)
+ xof1.update(b'90')
+ digest1 = xof1.read(100)
+
+ xof2 = xof1.new()
+ xof2.update(b'90')
+ digest2 = xof2.read(100)
+
+ self.assertEqual(digest1, digest2)
+
+ self.assertRaises(TypeError, xof1.new, domain=0x07)
+
+
+class TurboSHAKE128Test(TurboSHAKETest):
+ TurboSHAKE = TurboSHAKE128
+
+
+class TurboSHAKE256Test(TurboSHAKETest):
+ TurboSHAKE = TurboSHAKE256
+
+
+def txt2bin(txt):
+ clean = txt.replace(" ", "").replace("\n", "").replace("\r", "")
+ return unhexlify(clean)
+
+
+def ptn(n):
+ res = bytearray(n)
+ pattern = b"".join([bchr(x) for x in range(0, 0xFB)])
+ for base in range(0, n - 0xFB, 0xFB):
+ res[base:base + 0xFB] = pattern
+ remain = n % 0xFB
+ if remain:
+ base = (n // 0xFB) * 0xFB
+ res[base:] = pattern[:remain]
+ assert len(res) == n
+ return res
+
+
+def chunked(source, size):
+ for i in range(0, len(source), size):
+ yield source[i:i+size]
+
+
+class TurboSHAKE128TV(unittest.TestCase):
+
+ def test_zero_1(self):
+ tv = """1E 41 5F 1C 59 83 AF F2 16 92 17 27 7D 17 BB 53
+ 8C D9 45 A3 97 DD EC 54 1F 1C E4 1A F2 C1 B7 4C"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new().read(32)
+ self.assertEqual(res, btv)
+
+ def test_zero_2(self):
+ tv = """1E 41 5F 1C 59 83 AF F2 16 92 17 27 7D 17 BB 53
+ 8C D9 45 A3 97 DD EC 54 1F 1C E4 1A F2 C1 B7 4C
+ 3E 8C CA E2 A4 DA E5 6C 84 A0 4C 23 85 C0 3C 15
+ E8 19 3B DF 58 73 73 63 32 16 91 C0 54 62 C8 DF"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new().read(64)
+ self.assertEqual(res, btv)
+
+ def test_zero_3(self):
+ tv = """A3 B9 B0 38 59 00 CE 76 1F 22 AE D5 48 E7 54 DA
+ 10 A5 24 2D 62 E8 C6 58 E3 F3 A9 23 A7 55 56 07"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new().read(10032)[-32:]
+ self.assertEqual(res, btv)
+
+ def test_ptn_1(self):
+ tv = """55 CE DD 6F 60 AF 7B B2 9A 40 42 AE 83 2E F3 F5
+ 8D B7 29 9F 89 3E BB 92 47 24 7D 85 69 58 DA A9"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new(data=ptn(1)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17(self):
+ tv = """9C 97 D0 36 A3 BA C8 19 DB 70 ED E0 CA 55 4E C6
+ E4 C2 A1 A4 FF BF D9 EC 26 9C A6 A1 11 16 12 33"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new(data=ptn(17)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_2(self):
+ tv = """96 C7 7C 27 9E 01 26 F7 FC 07 C9 B0 7F 5C DA E1
+ E0 BE 60 BD BE 10 62 00 40 E7 5D 72 23 A6 24 D2"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new(data=ptn(17**2)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_3(self):
+ tv = """D4 97 6E B5 6B CF 11 85 20 58 2B 70 9F 73 E1 D6
+ 85 3E 00 1F DA F8 0E 1B 13 E0 D0 59 9D 5F B3 72"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new(data=ptn(17**3)).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_4(self):
+ tv = """DA 67 C7 03 9E 98 BF 53 0C F7 A3 78 30 C6 66 4E
+ 14 CB AB 7F 54 0F 58 40 3B 1B 82 95 13 18 EE 5C"""
+
+ btv = txt2bin(tv)
+ data = ptn(17**4)
+
+ # All at once
+ res = TurboSHAKE128.new(data=data).read(32)
+ self.assertEqual(res, btv)
+
+ # Byte by byte
+ xof = TurboSHAKE128.new()
+ for x in data:
+ xof.update(bchr(x))
+ res = xof.read(32)
+ self.assertEqual(res, btv)
+
+ # Chunks of various prime sizes
+ for chunk_size in (13, 17, 19, 23, 31):
+ xof = TurboSHAKE128.new()
+ for x in chunked(data, chunk_size):
+ xof.update(x)
+ res = xof.read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_5(self):
+ tv = """B9 7A 90 6F BF 83 EF 7C 81 25 17 AB F3 B2 D0 AE
+ A0 C4 F6 03 18 CE 11 CF 10 39 25 12 7F 59 EE CD"""
+
+ btv = txt2bin(tv)
+ data = ptn(17**5)
+
+ # All at once
+ res = TurboSHAKE128.new(data=data).read(32)
+ self.assertEqual(res, btv)
+
+ # Chunks
+ xof = TurboSHAKE128.new()
+ for chunk in chunked(data, 8192):
+ xof.update(chunk)
+ res = xof.read(32)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_6(self):
+ tv = """35 CD 49 4A DE DE D2 F2 52 39 AF 09 A7 B8 EF 0C
+ 4D 1C A4 FE 2D 1A C3 70 FA 63 21 6F E7 B4 C2 B1"""
+
+ btv = txt2bin(tv)
+ data = ptn(17**6)
+
+ res = TurboSHAKE128.new(data=data).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ffffff_d01(self):
+ tv = """BF 32 3F 94 04 94 E8 8E E1 C5 40 FE 66 0B E8 A0
+ C9 3F 43 D1 5E C0 06 99 84 62 FA 99 4E ED 5D AB"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new(data=b"\xff\xff\xff", domain=0x01).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ff_d06(self):
+ tv = """8E C9 C6 64 65 ED 0D 4A 6C 35 D1 35 06 71 8D 68
+ 7A 25 CB 05 C7 4C CA 1E 42 50 1A BD 83 87 4A 67"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new(data=b'\xFF', domain=0x06).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ffffff_d07(self):
+ tv = """B6 58 57 60 01 CA D9 B1 E5 F3 99 A9 F7 77 23 BB
+ A0 54 58 04 2D 68 20 6F 72 52 68 2D BA 36 63 ED"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new(data=b'\xFF' * 3, domain=0x07).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ffffffffffff_d0b(self):
+ tv = """8D EE AA 1A EC 47 CC EE 56 9F 65 9C 21 DF A8 E1
+ 12 DB 3C EE 37 B1 81 78 B2 AC D8 05 B7 99 CC 37"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new(data=b'\xFF' * 7, domain=0x0B).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ff_d30(self):
+ tv = """55 31 22 E2 13 5E 36 3C 32 92 BE D2 C6 42 1F A2
+ 32 BA B0 3D AA 07 C7 D6 63 66 03 28 65 06 32 5B"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new(data=b'\xFF', domain=0x30).read(32)
+ self.assertEqual(res, btv)
+
+ def test_ffffff_d7f(self):
+ tv = """16 27 4C C6 56 D4 4C EF D4 22 39 5D 0F 90 53 BD
+ A6 D2 8E 12 2A BA 15 C7 65 E5 AD 0E 6E AF 26 F9"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE128.new(data=b'\xFF' * 3, domain=0x7F).read(32)
+ self.assertEqual(res, btv)
+
+
+class TurboSHAKE256TV(unittest.TestCase):
+
+ def test_zero_1(self):
+ tv = """36 7A 32 9D AF EA 87 1C 78 02 EC 67 F9 05 AE 13
+ C5 76 95 DC 2C 66 63 C6 10 35 F5 9A 18 F8 E7 DB
+ 11 ED C0 E1 2E 91 EA 60 EB 6B 32 DF 06 DD 7F 00
+ 2F BA FA BB 6E 13 EC 1C C2 0D 99 55 47 60 0D B0"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new().read(64)
+ self.assertEqual(res, btv)
+
+ def test_zero_2(self):
+ tv = """AB EF A1 16 30 C6 61 26 92 49 74 26 85 EC 08 2F
+ 20 72 65 DC CF 2F 43 53 4E 9C 61 BA 0C 9D 1D 75"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new().read(10032)[-32:]
+ self.assertEqual(res, btv)
+
+ def test_ptn_1(self):
+ tv = """3E 17 12 F9 28 F8 EA F1 05 46 32 B2 AA 0A 24 6E
+ D8 B0 C3 78 72 8F 60 BC 97 04 10 15 5C 28 82 0E
+ 90 CC 90 D8 A3 00 6A A2 37 2C 5C 5E A1 76 B0 68
+ 2B F2 2B AE 74 67 AC 94 F7 4D 43 D3 9B 04 82 E2"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new(data=ptn(1)).read(64)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17(self):
+ tv = """B3 BA B0 30 0E 6A 19 1F BE 61 37 93 98 35 92 35
+ 78 79 4E A5 48 43 F5 01 10 90 FA 2F 37 80 A9 E5
+ CB 22 C5 9D 78 B4 0A 0F BF F9 E6 72 C0 FB E0 97
+ 0B D2 C8 45 09 1C 60 44 D6 87 05 4D A5 D8 E9 C7"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new(data=ptn(17)).read(64)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_2(self):
+ tv = """66 B8 10 DB 8E 90 78 04 24 C0 84 73 72 FD C9 57
+ 10 88 2F DE 31 C6 DF 75 BE B9 D4 CD 93 05 CF CA
+ E3 5E 7B 83 E8 B7 E6 EB 4B 78 60 58 80 11 63 16
+ FE 2C 07 8A 09 B9 4A D7 B8 21 3C 0A 73 8B 65 C0"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new(data=ptn(17**2)).read(64)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_3(self):
+ tv = """C7 4E BC 91 9A 5B 3B 0D D1 22 81 85 BA 02 D2 9E
+ F4 42 D6 9D 3D 42 76 A9 3E FE 0B F9 A1 6A 7D C0
+ CD 4E AB AD AB 8C D7 A5 ED D9 66 95 F5 D3 60 AB
+ E0 9E 2C 65 11 A3 EC 39 7D A3 B7 6B 9E 16 74 FB"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new(data=ptn(17**3)).read(64)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_4(self):
+ tv = """02 CC 3A 88 97 E6 F4 F6 CC B6 FD 46 63 1B 1F 52
+ 07 B6 6C 6D E9 C7 B5 5B 2D 1A 23 13 4A 17 0A FD
+ AC 23 4E AB A9 A7 7C FF 88 C1 F0 20 B7 37 24 61
+ 8C 56 87 B3 62 C4 30 B2 48 CD 38 64 7F 84 8A 1D"""
+
+ btv = txt2bin(tv)
+ data = ptn(17**4)
+
+ # All at once
+ res = TurboSHAKE256.new(data=data).read(64)
+ self.assertEqual(res, btv)
+
+ # Byte by byte
+ xof = TurboSHAKE256.new()
+ for x in data:
+ xof.update(bchr(x))
+ res = xof.read(64)
+ self.assertEqual(res, btv)
+
+ # Chunks of various prime sizes
+ for chunk_size in (13, 17, 19, 23, 31):
+ xof = TurboSHAKE256.new()
+ for x in chunked(data, chunk_size):
+ xof.update(x)
+ res = xof.read(64)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_5(self):
+ tv = """AD D5 3B 06 54 3E 58 4B 58 23 F6 26 99 6A EE 50
+ FE 45 ED 15 F2 02 43 A7 16 54 85 AC B4 AA 76 B4
+ FF DA 75 CE DF 6D 8C DC 95 C3 32 BD 56 F4 B9 86
+ B5 8B B1 7D 17 78 BF C1 B1 A9 75 45 CD F4 EC 9F"""
+
+ btv = txt2bin(tv)
+ data = ptn(17**5)
+
+ # All at once
+ res = TurboSHAKE256.new(data=data).read(64)
+ self.assertEqual(res, btv)
+
+ # Chunks
+ xof = TurboSHAKE256.new()
+ for chunk in chunked(data, 8192):
+ xof.update(chunk)
+ res = xof.read(64)
+ self.assertEqual(res, btv)
+
+ def test_ptn_17_6(self):
+ tv = """9E 11 BC 59 C2 4E 73 99 3C 14 84 EC 66 35 8E F7
+ 1D B7 4A EF D8 4E 12 3F 78 00 BA 9C 48 53 E0 2C
+ FE 70 1D 9E 6B B7 65 A3 04 F0 DC 34 A4 EE 3B A8
+ 2C 41 0F 0D A7 0E 86 BF BD 90 EA 87 7C 2D 61 04"""
+
+ btv = txt2bin(tv)
+ data = ptn(17**6)
+
+ res = TurboSHAKE256.new(data=data).read(64)
+ self.assertEqual(res, btv)
+
+ def test_ffffff_d01(self):
+ tv = """D2 1C 6F BB F5 87 FA 22 82 F2 9A EA 62 01 75 FB
+ 02 57 41 3A F7 8A 0B 1B 2A 87 41 9C E0 31 D9 33
+ AE 7A 4D 38 33 27 A8 A1 76 41 A3 4F 8A 1D 10 03
+ AD 7D A6 B7 2D BA 84 BB 62 FE F2 8F 62 F1 24 24"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new(data=b"\xff\xff\xff", domain=0x01).read(64)
+ self.assertEqual(res, btv)
+
+ def test_ff_d06(self):
+ tv = """73 8D 7B 4E 37 D1 8B 7F 22 AD 1B 53 13 E3 57 E3
+ DD 7D 07 05 6A 26 A3 03 C4 33 FA 35 33 45 52 80
+ F4 F5 A7 D4 F7 00 EF B4 37 FE 6D 28 14 05 E0 7B
+ E3 2A 0A 97 2E 22 E6 3A DC 1B 09 0D AE FE 00 4B"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new(data=b'\xFF', domain=0x06).read(64)
+ self.assertEqual(res, btv)
+
+ def test_ffffff_d07(self):
+ tv = """18 B3 B5 B7 06 1C 2E 67 C1 75 3A 00 E6 AD 7E D7
+ BA 1C 90 6C F9 3E FB 70 92 EA F2 7F BE EB B7 55
+ AE 6E 29 24 93 C1 10 E4 8D 26 00 28 49 2B 8E 09
+ B5 50 06 12 B8 F2 57 89 85 DE D5 35 7D 00 EC 67"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new(data=b'\xFF' * 3, domain=0x07).read(64)
+ self.assertEqual(res, btv)
+
+ def test_ffffffffffff_d0b(self):
+ tv = """BB 36 76 49 51 EC 97 E9 D8 5F 7E E9 A6 7A 77 18
+ FC 00 5C F4 25 56 BE 79 CE 12 C0 BD E5 0E 57 36
+ D6 63 2B 0D 0D FB 20 2D 1B BB 8F FE 3D D7 4C B0
+ 08 34 FA 75 6C B0 34 71 BA B1 3A 1E 2C 16 B3 C0"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new(data=b'\xFF' * 7, domain=0x0B).read(64)
+ self.assertEqual(res, btv)
+
+ def test_ff_d30(self):
+ tv = """F3 FE 12 87 3D 34 BC BB 2E 60 87 79 D6 B7 0E 7F
+ 86 BE C7 E9 0B F1 13 CB D4 FD D0 C4 E2 F4 62 5E
+ 14 8D D7 EE 1A 52 77 6C F7 7F 24 05 14 D9 CC FC
+ 3B 5D DA B8 EE 25 5E 39 EE 38 90 72 96 2C 11 1A"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new(data=b'\xFF', domain=0x30).read(64)
+ self.assertEqual(res, btv)
+
+ def test_ffffff_d7f(self):
+ tv = """AB E5 69 C1 F7 7E C3 40 F0 27 05 E7 D3 7C 9A B7
+ E1 55 51 6E 4A 6A 15 00 21 D7 0B 6F AC 0B B4 0C
+ 06 9F 9A 98 28 A0 D5 75 CD 99 F9 BA E4 35 AB 1A
+ CF 7E D9 11 0B A9 7C E0 38 8D 07 4B AC 76 87 76"""
+
+ btv = txt2bin(tv)
+ res = TurboSHAKE256.new(data=b'\xFF' * 3, domain=0x7F).read(64)
+ self.assertEqual(res, btv)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TurboSHAKE128Test)
+ tests += list_test_cases(TurboSHAKE256Test)
+ tests += list_test_cases(TurboSHAKE128TV)
+ tests += list_test_cases(TurboSHAKE256TV)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_cSHAKE.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_cSHAKE.py
new file mode 100644
index 00000000..72ad3411
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_cSHAKE.py
@@ -0,0 +1,178 @@
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.cSHAKE128 and cSHAKE256"""
+
+import unittest
+
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Hash import cSHAKE128, cSHAKE256, SHAKE128, SHAKE256
+from Crypto.Util.py3compat import b, bchr, tobytes
+
+
+class cSHAKETest(unittest.TestCase):
+
+ def test_left_encode(self):
+ from Crypto.Hash.cSHAKE128 import _left_encode
+ self.assertEqual(_left_encode(0), b'\x01\x00')
+ self.assertEqual(_left_encode(1), b'\x01\x01')
+ self.assertEqual(_left_encode(256), b'\x02\x01\x00')
+
+ def test_bytepad(self):
+ from Crypto.Hash.cSHAKE128 import _bytepad
+ self.assertEqual(_bytepad(b'', 4), b'\x01\x04\x00\x00')
+ self.assertEqual(_bytepad(b'A', 4), b'\x01\x04A\x00')
+ self.assertEqual(_bytepad(b'AA', 4), b'\x01\x04AA')
+ self.assertEqual(_bytepad(b'AAA', 4), b'\x01\x04AAA\x00\x00\x00')
+ self.assertEqual(_bytepad(b'AAAA', 4), b'\x01\x04AAAA\x00\x00')
+ self.assertEqual(_bytepad(b'AAAAA', 4), b'\x01\x04AAAAA\x00')
+ self.assertEqual(_bytepad(b'AAAAAA', 4), b'\x01\x04AAAAAA')
+ self.assertEqual(_bytepad(b'AAAAAAA', 4), b'\x01\x04AAAAAAA\x00\x00\x00')
+
+ def test_new_positive(self):
+
+ xof1 = self.cshake.new()
+ xof2 = self.cshake.new(data=b("90"))
+ xof3 = self.cshake.new().update(b("90"))
+
+ self.assertNotEqual(xof1.read(10), xof2.read(10))
+ xof3.read(10)
+ self.assertEqual(xof2.read(10), xof3.read(10))
+
+ xof1 = self.cshake.new()
+ ref = xof1.read(10)
+ xof2 = self.cshake.new(custom=b(""))
+ xof3 = self.cshake.new(custom=b("foo"))
+
+ self.assertEqual(ref, xof2.read(10))
+ self.assertNotEqual(ref, xof3.read(10))
+
+ xof1 = self.cshake.new(custom=b("foo"))
+ xof2 = self.cshake.new(custom=b("foo"), data=b("90"))
+ xof3 = self.cshake.new(custom=b("foo")).update(b("90"))
+
+ self.assertNotEqual(xof1.read(10), xof2.read(10))
+ xof3.read(10)
+ self.assertEqual(xof2.read(10), xof3.read(10))
+
+ def test_update(self):
+ pieces = [bchr(10) * 200, bchr(20) * 300]
+ h = self.cshake.new()
+ h.update(pieces[0]).update(pieces[1])
+ digest = h.read(10)
+ h = self.cshake.new()
+ h.update(pieces[0] + pieces[1])
+ self.assertEqual(h.read(10), digest)
+
+ def test_update_negative(self):
+ h = self.cshake.new()
+ self.assertRaises(TypeError, h.update, u"string")
+
+ def test_digest(self):
+ h = self.cshake.new()
+ digest = h.read(90)
+
+ # read returns a byte string of the right length
+ self.assertTrue(isinstance(digest, type(b("digest"))))
+ self.assertEqual(len(digest), 90)
+
+ def test_update_after_read(self):
+ mac = self.cshake.new()
+ mac.update(b("rrrr"))
+ mac.read(90)
+ self.assertRaises(TypeError, mac.update, b("ttt"))
+
+ def test_shake(self):
+ # When no customization string is passed, results must match SHAKE
+ for digest_len in range(64):
+ xof1 = self.cshake.new(b'TEST')
+ xof2 = self.shake.new(b'TEST')
+ self.assertEqual(xof1.read(digest_len), xof2.read(digest_len))
+
+
+class cSHAKE128Test(cSHAKETest):
+ cshake = cSHAKE128
+ shake = SHAKE128
+
+
+class cSHAKE256Test(cSHAKETest):
+ cshake = cSHAKE256
+ shake = SHAKE256
+
+
+class cSHAKEVectors(unittest.TestCase):
+ pass
+
+
+vector_files = [("ShortMsgSamples_cSHAKE128.txt", "Short Message Samples cSHAKE128", "128_cshake", cSHAKE128),
+ ("ShortMsgSamples_cSHAKE256.txt", "Short Message Samples cSHAKE256", "256_cshake", cSHAKE256),
+ ("CustomMsgSamples_cSHAKE128.txt", "Custom Message Samples cSHAKE128", "custom_128_cshake", cSHAKE128),
+ ("CustomMsgSamples_cSHAKE256.txt", "Custom Message Samples cSHAKE256", "custom_256_cshake", cSHAKE256),
+ ]
+
+for file, descr, tag, test_class in vector_files:
+
+ test_vectors = load_test_vectors(("Hash", "SHA3"), file, descr,
+ {"len": lambda x: int(x),
+ "nlen": lambda x: int(x),
+ "slen": lambda x: int(x)}) or []
+
+ for idx, tv in enumerate(test_vectors):
+ if getattr(tv, "len", 0) == 0:
+ data = b("")
+ else:
+ data = tobytes(tv.msg)
+ assert(tv.len == len(tv.msg)*8)
+ if getattr(tv, "nlen", 0) != 0:
+ raise ValueError("Unsupported cSHAKE test vector")
+ if getattr(tv, "slen", 0) == 0:
+ custom = b("")
+ else:
+ custom = tobytes(tv.s)
+ assert(tv.slen == len(tv.s)*8)
+
+ def new_test(self, data=data, result=tv.md, custom=custom, test_class=test_class):
+ hobj = test_class.new(data=data, custom=custom)
+ digest = hobj.read(len(result))
+ self.assertEqual(digest, result)
+
+ setattr(cSHAKEVectors, "test_%s_%d" % (tag, idx), new_test)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(cSHAKE128Test)
+ tests += list_test_cases(cSHAKE256Test)
+ tests += list_test_cases(cSHAKEVectors)
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_keccak.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_keccak.py
new file mode 100644
index 00000000..54cdf27d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Hash/test_keccak.py
@@ -0,0 +1,250 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Hash.keccak"""
+
+import unittest
+from binascii import hexlify, unhexlify
+
+from Crypto.SelfTest.loader import load_test_vectors
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Hash import keccak
+from Crypto.Util.py3compat import b, tobytes, bchr
+
+class KeccakTest(unittest.TestCase):
+
+ def test_new_positive(self):
+
+ for digest_bits in (224, 256, 384, 512):
+ hobj = keccak.new(digest_bits=digest_bits)
+ self.assertEqual(hobj.digest_size, digest_bits // 8)
+
+ hobj2 = hobj.new()
+ self.assertEqual(hobj2.digest_size, digest_bits // 8)
+
+ for digest_bytes in (28, 32, 48, 64):
+ hobj = keccak.new(digest_bytes=digest_bytes)
+ self.assertEqual(hobj.digest_size, digest_bytes)
+
+ hobj2 = hobj.new()
+ self.assertEqual(hobj2.digest_size, digest_bytes)
+
+ def test_new_positive2(self):
+
+ digest1 = keccak.new(data=b("\x90"), digest_bytes=64).digest()
+ digest2 = keccak.new(digest_bytes=64).update(b("\x90")).digest()
+ self.assertEqual(digest1, digest2)
+
+ def test_new_negative(self):
+
+ # keccak.new needs digest size
+ self.assertRaises(TypeError, keccak.new)
+
+ h = keccak.new(digest_bits=512)
+
+ # Either bits or bytes can be specified
+ self.assertRaises(TypeError, keccak.new,
+ digest_bytes=64,
+ digest_bits=512)
+
+ # Range
+ self.assertRaises(ValueError, keccak.new, digest_bytes=0)
+ self.assertRaises(ValueError, keccak.new, digest_bytes=1)
+ self.assertRaises(ValueError, keccak.new, digest_bytes=65)
+ self.assertRaises(ValueError, keccak.new, digest_bits=0)
+ self.assertRaises(ValueError, keccak.new, digest_bits=1)
+ self.assertRaises(ValueError, keccak.new, digest_bits=513)
+
+ def test_update(self):
+ pieces = [bchr(10) * 200, bchr(20) * 300]
+ h = keccak.new(digest_bytes=64)
+ h.update(pieces[0]).update(pieces[1])
+ digest = h.digest()
+ h = keccak.new(digest_bytes=64)
+ h.update(pieces[0] + pieces[1])
+ self.assertEqual(h.digest(), digest)
+
+ def test_update_negative(self):
+ h = keccak.new(digest_bytes=64)
+ self.assertRaises(TypeError, h.update, u"string")
+
+ def test_digest(self):
+ h = keccak.new(digest_bytes=64)
+ digest = h.digest()
+
+ # hexdigest does not change the state
+ self.assertEqual(h.digest(), digest)
+ # digest returns a byte string
+ self.assertTrue(isinstance(digest, type(b("digest"))))
+
+ def test_hex_digest(self):
+ mac = keccak.new(digest_bits=512)
+ digest = mac.digest()
+ hexdigest = mac.hexdigest()
+
+ # hexdigest is equivalent to digest
+ self.assertEqual(hexlify(digest), tobytes(hexdigest))
+ # hexdigest does not change the state
+ self.assertEqual(mac.hexdigest(), hexdigest)
+ # hexdigest returns a string
+ self.assertTrue(isinstance(hexdigest, type("digest")))
+
+ def test_update_after_digest(self):
+ msg=b("rrrrttt")
+
+ # Normally, update() cannot be done after digest()
+ h = keccak.new(digest_bits=512, data=msg[:4])
+ dig1 = h.digest()
+ self.assertRaises(TypeError, h.update, msg[4:])
+ dig2 = keccak.new(digest_bits=512, data=msg).digest()
+
+ # With the proper flag, it is allowed
+ h = keccak.new(digest_bits=512, data=msg[:4], update_after_digest=True)
+ self.assertEqual(h.digest(), dig1)
+ # ... and the subsequent digest applies to the entire message
+ # up to that point
+ h.update(msg[4:])
+ self.assertEqual(h.digest(), dig2)
+
+
+class KeccakVectors(unittest.TestCase):
+ pass
+
+ # TODO: add ExtremelyLong tests
+
+
+test_vectors_224 = load_test_vectors(("Hash", "keccak"),
+ "ShortMsgKAT_224.txt",
+ "Short Messages KAT 224",
+ {"len": lambda x: int(x)}) or []
+
+test_vectors_224 += load_test_vectors(("Hash", "keccak"),
+ "LongMsgKAT_224.txt",
+ "Long Messages KAT 224",
+ {"len": lambda x: int(x)}) or []
+
+for idx, tv in enumerate(test_vectors_224):
+ if tv.len == 0:
+ data = b("")
+ else:
+ data = tobytes(tv.msg)
+
+ def new_test(self, data=data, result=tv.md):
+ hobj = keccak.new(digest_bits=224, data=data)
+ self.assertEqual(hobj.digest(), result)
+
+ setattr(KeccakVectors, "test_224_%d" % idx, new_test)
+
+# ---
+
+test_vectors_256 = load_test_vectors(("Hash", "keccak"),
+ "ShortMsgKAT_256.txt",
+ "Short Messages KAT 256",
+ { "len" : lambda x: int(x) } ) or []
+
+test_vectors_256 += load_test_vectors(("Hash", "keccak"),
+ "LongMsgKAT_256.txt",
+ "Long Messages KAT 256",
+ { "len" : lambda x: int(x) } ) or []
+
+for idx, tv in enumerate(test_vectors_256):
+ if tv.len == 0:
+ data = b("")
+ else:
+ data = tobytes(tv.msg)
+
+ def new_test(self, data=data, result=tv.md):
+ hobj = keccak.new(digest_bits=256, data=data)
+ self.assertEqual(hobj.digest(), result)
+
+ setattr(KeccakVectors, "test_256_%d" % idx, new_test)
+
+
+# ---
+
+test_vectors_384 = load_test_vectors(("Hash", "keccak"),
+ "ShortMsgKAT_384.txt",
+ "Short Messages KAT 384",
+ {"len": lambda x: int(x)}) or []
+
+test_vectors_384 += load_test_vectors(("Hash", "keccak"),
+ "LongMsgKAT_384.txt",
+ "Long Messages KAT 384",
+ {"len": lambda x: int(x)}) or []
+
+for idx, tv in enumerate(test_vectors_384):
+ if tv.len == 0:
+ data = b("")
+ else:
+ data = tobytes(tv.msg)
+
+ def new_test(self, data=data, result=tv.md):
+ hobj = keccak.new(digest_bits=384, data=data)
+ self.assertEqual(hobj.digest(), result)
+
+ setattr(KeccakVectors, "test_384_%d" % idx, new_test)
+
+# ---
+
+test_vectors_512 = load_test_vectors(("Hash", "keccak"),
+ "ShortMsgKAT_512.txt",
+ "Short Messages KAT 512",
+ {"len": lambda x: int(x)}) or []
+
+test_vectors_512 += load_test_vectors(("Hash", "keccak"),
+ "LongMsgKAT_512.txt",
+ "Long Messages KAT 512",
+ {"len": lambda x: int(x)}) or []
+
+for idx, tv in enumerate(test_vectors_512):
+ if tv.len == 0:
+ data = b("")
+ else:
+ data = tobytes(tv.msg)
+
+ def new_test(self, data=data, result=tv.md):
+ hobj = keccak.new(digest_bits=512, data=data)
+ self.assertEqual(hobj.digest(), result)
+
+ setattr(KeccakVectors, "test_512_%d" % idx, new_test)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(KeccakTest)
+ tests += list_test_cases(KeccakVectors)
+ return tests
+
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/IO/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/IO/__init__.py
new file mode 100644
index 00000000..c04a2a78
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/IO/__init__.py
@@ -0,0 +1,47 @@
+#
+# SelfTest/IO/__init__.py: Self-test for input/output module
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""Self-test for I/O"""
+
+def get_tests(config={}):
+ tests = []
+ from Crypto.SelfTest.IO import test_PKCS8; tests += test_PKCS8.get_tests(config=config)
+ from Crypto.SelfTest.IO import test_PBES; tests += test_PBES.get_tests(config=config)
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/IO/test_PBES.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/IO/test_PBES.py
new file mode 100644
index 00000000..029e824a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/IO/test_PBES.py
@@ -0,0 +1,118 @@
+#
+# SelfTest/IO/test_PBES.py: Self-test for the _PBES module
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""Self-tests for Crypto.IO._PBES module"""
+
+import unittest
+
+from Crypto.IO._PBES import PBES2
+
+
+class TestPBES2(unittest.TestCase):
+
+ def setUp(self):
+ self.ref = b"Test data"
+ self.passphrase = b"Passphrase"
+
+ def test1(self):
+ ct = PBES2.encrypt(self.ref, self.passphrase,
+ 'PBKDF2WithHMAC-SHA1AndDES-EDE3-CBC')
+ pt = PBES2.decrypt(ct, self.passphrase)
+ self.assertEqual(self.ref, pt)
+
+ def test2(self):
+ ct = PBES2.encrypt(self.ref, self.passphrase,
+ 'PBKDF2WithHMAC-SHA224AndAES128-CBC')
+ pt = PBES2.decrypt(ct, self.passphrase)
+ self.assertEqual(self.ref, pt)
+
+ def test3(self):
+ ct = PBES2.encrypt(self.ref, self.passphrase,
+ 'PBKDF2WithHMAC-SHA256AndAES192-CBC')
+ pt = PBES2.decrypt(ct, self.passphrase)
+ self.assertEqual(self.ref, pt)
+
+ def test4(self):
+ ct = PBES2.encrypt(self.ref, self.passphrase,
+ 'PBKDF2WithHMAC-SHA384AndAES256-CBC')
+ pt = PBES2.decrypt(ct, self.passphrase)
+ self.assertEqual(self.ref, pt)
+
+ def test5(self):
+ ct = PBES2.encrypt(self.ref, self.passphrase,
+ 'PBKDF2WithHMAC-SHA512AndAES128-GCM')
+ pt = PBES2.decrypt(ct, self.passphrase)
+ self.assertEqual(self.ref, pt)
+
+ def test6(self):
+ ct = PBES2.encrypt(self.ref, self.passphrase,
+ 'PBKDF2WithHMAC-SHA512-224AndAES192-GCM')
+ pt = PBES2.decrypt(ct, self.passphrase)
+ self.assertEqual(self.ref, pt)
+
+ def test7(self):
+ ct = PBES2.encrypt(self.ref, self.passphrase,
+ 'PBKDF2WithHMAC-SHA3-256AndAES256-GCM')
+ pt = PBES2.decrypt(ct, self.passphrase)
+ self.assertEqual(self.ref, pt)
+
+ def test8(self):
+ ct = PBES2.encrypt(self.ref, self.passphrase,
+ 'scryptAndAES128-CBC')
+ pt = PBES2.decrypt(ct, self.passphrase)
+ self.assertEqual(self.ref, pt)
+
+ def test9(self):
+ ct = PBES2.encrypt(self.ref, self.passphrase,
+ 'scryptAndAES192-CBC')
+ pt = PBES2.decrypt(ct, self.passphrase)
+ self.assertEqual(self.ref, pt)
+
+ def test10(self):
+ ct = PBES2.encrypt(self.ref, self.passphrase,
+ 'scryptAndAES256-CBC')
+ pt = PBES2.decrypt(ct, self.passphrase)
+ self.assertEqual(self.ref, pt)
+
+
+def get_tests(config={}):
+ from Crypto.SelfTest.st_common import list_test_cases
+ listTests = []
+ listTests += list_test_cases(TestPBES2)
+ return listTests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/IO/test_PKCS8.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/IO/test_PKCS8.py
new file mode 100644
index 00000000..3525796b
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/IO/test_PKCS8.py
@@ -0,0 +1,459 @@
+#
+# SelfTest/IO/test_PKCS8.py: Self-test for the PKCS8 module
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""Self-tests for Crypto.IO.PKCS8 module"""
+
+import unittest
+from binascii import unhexlify
+
+from Crypto.Util.py3compat import *
+from Crypto.IO import PKCS8
+
+from Crypto.Util.asn1 import DerNull
+
+oid_key = '1.2.840.113549.1.1.1'
+
+# Original RSA key (in DER format)
+# hexdump -v -e '32/1 "%02x" "\n"' key.der
+clear_key="""
+308201ab020100025a00b94a7f7075ab9e79e8196f47be707781e80dd965cf16
+0c951a870b71783b6aaabbd550c0e65e5a3dfe15b8620009f6d7e5efec42a3f0
+6fe20faeebb0c356e79cdec6db4dd427e82d8ae4a5b90996227b8ba54ccfc4d2
+5c08050203010001025a00afa09c70d528299b7552fe766b5d20f9a221d66938
+c3b68371d48515359863ff96f0978d700e08cd6fd3d8a3f97066fc2e0d5f78eb
+3a50b8e17ba297b24d1b8e9cdfd18d608668198d724ad15863ef0329195dee89
+3f039395022d0ebe0518df702a8b25954301ec60a97efdcec8eaa4f2e76ca7e8
+8dfbc3f7e0bb83f9a0e8dc47c0f8c746e9df6b022d0c9195de13f09b7be1fdd7
+1f56ae7d973e08bd9fd2c3dfd8936bb05be9cc67bd32d663c7f00d70932a0be3
+c24f022d0ac334eb6cabf1933633db007b763227b0d9971a9ea36aca8b669ec9
+4fcf16352f6b3dcae28e4bd6137db4ddd3022d0400a09f15ee7b351a2481cb03
+09920905c236d09c87afd3022f3afc2a19e3b746672b635238956ee7e6dd62d5
+022d0cd88ed14fcfbda5bbf0257f700147137bbab9c797af7df866704b889aa3
+7e2e93df3ff1a0fd3490111dcdbc4c
+"""
+
+# Same key as above, wrapped in PKCS#8 but w/o password
+#
+# openssl pkcs8 -topk8 -inform DER -nocrypt -in key.der -outform DER -out keyp8.der
+# hexdump -v -e '32/1 "%02x" "\n"' keyp8.der
+wrapped_clear_key="""
+308201c5020100300d06092a864886f70d0101010500048201af308201ab0201
+00025a00b94a7f7075ab9e79e8196f47be707781e80dd965cf160c951a870b71
+783b6aaabbd550c0e65e5a3dfe15b8620009f6d7e5efec42a3f06fe20faeebb0
+c356e79cdec6db4dd427e82d8ae4a5b90996227b8ba54ccfc4d25c0805020301
+0001025a00afa09c70d528299b7552fe766b5d20f9a221d66938c3b68371d485
+15359863ff96f0978d700e08cd6fd3d8a3f97066fc2e0d5f78eb3a50b8e17ba2
+97b24d1b8e9cdfd18d608668198d724ad15863ef0329195dee893f039395022d
+0ebe0518df702a8b25954301ec60a97efdcec8eaa4f2e76ca7e88dfbc3f7e0bb
+83f9a0e8dc47c0f8c746e9df6b022d0c9195de13f09b7be1fdd71f56ae7d973e
+08bd9fd2c3dfd8936bb05be9cc67bd32d663c7f00d70932a0be3c24f022d0ac3
+34eb6cabf1933633db007b763227b0d9971a9ea36aca8b669ec94fcf16352f6b
+3dcae28e4bd6137db4ddd3022d0400a09f15ee7b351a2481cb0309920905c236
+d09c87afd3022f3afc2a19e3b746672b635238956ee7e6dd62d5022d0cd88ed1
+4fcfbda5bbf0257f700147137bbab9c797af7df866704b889aa37e2e93df3ff1
+a0fd3490111dcdbc4c
+"""
+
+###
+#
+# The key above will now be encrypted with different algorithms.
+# The password is always 'TestTest'.
+#
+# Each item in the wrapped_enc_keys list contains:
+# * wrap algorithm
+# * iteration count
+# * Salt
+# * IV
+# * Expected result
+###
+wrapped_enc_keys = []
+
+#
+# openssl pkcs8 -topk8 -passin pass:TestTest -inform DER -in key.der -outform DER -out keyenc.der -v2 des3
+# hexdump -v -e '32/1 "%02x" "\n"' keyenc.der
+#
+wrapped_enc_keys.append((
+'PBKDF2WithHMAC-SHA1AndDES-EDE3-CBC',
+2048,
+"47EA7227D8B22E2F", # IV
+"E3F7A838AB911A4D", # Salt
+"""
+30820216304006092a864886f70d01050d3033301b06092a864886f70d01050c
+300e0408e3f7a838ab911a4d02020800301406082a864886f70d0307040847ea
+7227d8b22e2f048201d0ea388b374d2d0e4ceb7a5139f850fdff274884a6e6c0
+64326e09d00dbba9018834edb5a51a6ae3d1806e6e91eebf33788ce71fee0637
+a2ebf58859dd32afc644110c390274a6128b50c39b8d907823810ec471bada86
+6f5b75d8ea04ad310fad2e73621696db8e426cd511ee93ec1714a1a7db45e036
+4bf20d178d1f16bbb250b32c2d200093169d588de65f7d99aad9ddd0104b44f1
+326962e1520dfac3c2a800e8a14f678dff2b3d0bb23f69da635bf2a643ac934e
+219a447d2f4460b67149e860e54f365da130763deefa649c72b0dcd48966a2d3
+4a477444782e3e66df5a582b07bbb19778a79bd355074ce331f4a82eb966b0c4
+52a09eab6116f2722064d314ae433b3d6e81d2436e93fdf446112663cde93b87
+9c8be44beb45f18e2c78fee9b016033f01ecda51b9b142091fa69f65ab784d2c
+5ad8d34be6f7f1464adfc1e0ef3f7848f40d3bdea4412758f2fcb655c93d8f4d
+f6fa48fc5aa4b75dd1c017ab79ac9d737233a6d668f5364ccf47786debd37334
+9c10c9e6efbe78430a61f71c89948aa32cdc3cc7338cf994147819ce7ab23450
+c8f7d9b94c3bb377d17a3fa204b601526317824b142ff6bc843fa7815ece89c0
+839573f234dac8d80cc571a045353d61db904a4398d8ef3df5ac
+"""
+))
+
+#
+# openssl pkcs8 -topk8 -passin pass:TestTest -inform DER -in key.der -outform DER -out keyenc.der
+# hexdump -v -e '32/1 "%02x" "\n"' keyenc.der
+#
+wrapped_enc_keys.append((
+'skip encryption', # pbeWithMD5AndDES-CBC, only decoding is supported
+-1,
+"",
+"",
+"""
+308201f1301b06092a864886f70d010503300e0408f9b990c89af1d41b020208
+00048201d0c6267fe8592903891933d559e71a7ca68b2e39150f19daca0f7921
+52f97e249d72f670d5140e9150433310ed7c7ee51927693fd39884cb9551cea5
+a7b746f7edf199f8787d4787a35dad930d7db057b2118851211b645ac8b90fa6
+b0e7d49ac8567cbd5fff226e87aa9129a0f52c45e9307752e8575c3b0ff756b7
+31fda6942d15ecb6b27ea19370ccc79773f47891e80d22b440d81259c4c28eac
+e0ca839524116bcf52d8c566e49a95ddb0e5493437279a770a39fd333f3fca91
+55884fad0ba5aaf273121f893059d37dd417da7dcfd0d6fa7494968f13b2cc95
+65633f2c891340193e5ec00e4ee0b0e90b3b93da362a4906360845771ade1754
+9df79140be5993f3424c012598eadd3e7c7c0b4db2c72cf103d7943a5cf61420
+93370b9702386c3dd4eb0a47f34b579624a46a108b2d13921fa1b367495fe345
+6aa128aa70f8ca80ae13eb301e96c380724ce67c54380bbea2316c1faf4d058e
+b4ca2e23442047606b9bc4b3bf65b432cb271bea4eb35dd3eb360d3be8612a87
+a50e96a2264490aeabdc07c6e78e5dbf4fe3388726d0e2a228346bf3c2907d68
+2a6276b22ae883fb30fa611f4e4193e7a08480fcd7db48308bacbd72bf4807aa
+11fd394859f97d22982f7fe890b2e2a0f7e7ffb693
+"""
+))
+
+#
+# openssl pkcs8 -topk8 -passin pass:TestTest -inform DER -in key.der
+# -outform DER -out keyenc.der -v1 PBE-SHA1-RC2-64
+# hexdump -v -e '32/1 "%02x" "\n"' keyenc.der
+#
+wrapped_enc_keys.append((
+'skip encryption', # pbeWithSHA1AndRC2-CBC, only decoding is supported
+-1,
+"",
+"",
+"""
+308201f1301b06092a864886f70d01050b300e04083ee943bdae185008020208
+00048201d0e4614d9371d3ff10ceabc2f6a7a13a0f449f9a714144e46518ea55
+e3e6f0cde24031d01ef1f37ec40081449ef01914faf45983dde0d2bc496712de
+8dd15a5527dff4721d9016c13f34fb93e3ce68577e30146266d71b539f854e56
+753a192cf126ed4812734d86f81884374f1100772f78d0646e9946407637c565
+d070acab413c55952f7237437f2e48cae7fa0ff8d370de2bf446dd08049a3663
+d9c813ac197468c02e2b687e7ca994cf7f03f01b6eca87dbfed94502c2094157
+ea39f73fe4e591df1a68b04d19d9adab90bb9898467c1464ad20bf2b8fb9a5ff
+d3ec91847d1c67fd768a4b9cfb46572eccc83806601372b6fad0243f58f623b7
+1c5809dea0feb8278fe27e5560eed8448dc93f5612f546e5dd7c5f6404365eb2
+5bf3396814367ae8b15c5c432b57eaed1f882c05c7f6517ee9e42b87b7b8d071
+9d6125d1b52f7b2cca1f6bd5f584334bf90bce1a7d938274cafe27b68e629698
+b16e27ae528db28593af9adcfccbebb3b9e1f2af5cd5531b51968389caa6c091
+e7de1f1b96f0d258e54e540d961a7c0ef51fda45d6da5fddd33e9bbfd3a5f8d7
+d7ab2e971de495cddbc86d38444fee9f0ac097b00adaf7802dabe0cff5b43b45
+4f26b7b547016f89be52676866189911c53e2f2477"""
+))
+
+#
+# openssl pkcs8 -topk8 -passin pass:TestTest -inform DER -in key.der
+# -outform DER -out keyenc.der -v1 PBE-MD5-RC2-64
+# hexdump -v -e '32/1 "%02x" "\n"' keyenc.der
+#
+wrapped_enc_keys.append((
+'skip encryption', # pbeWithMD5AndRC2-CBC, only decoding is supported
+-1,
+"",
+"",
+"""
+308201f1301b06092a864886f70d010506300e0408f5cd2fee56d9b4b8020208
+00048201d086454942d6166a19d6b108465bd111e7080911f573d54b1369c676
+df28600e84936bfec04f91023ff16499e2e07178c340904f12ffa6886ab66228
+32bf43c2bff5a0ed14e765918cf5fc543ad49566246f7eb3fc044fa5a9c25f40
+8fc8c8296b91658d3bb1067c0aba008c4fefd9e2bcdbbbd63fdc8085482bccf4
+f150cec9a084259ad441a017e5d81a1034ef2484696a7a50863836d0eeda45cd
+8cee8ecabfed703f8d9d4bbdf3a767d32a0ccdc38550ee2928d7fe3fa27eda5b
+5c7899e75ad55d076d2c2d3c37d6da3d95236081f9671dab9a99afdb1cbc890e
+332d1a91105d9a8ce08b6027aa07367bd1daec3059cb51f5d896124da16971e4
+0ca4bcadb06c854bdf39f42dd24174011414e51626d198775eff3449a982df7b
+ace874e77e045eb6d7c3faef0750792b29a068a6291f7275df1123fac5789c51
+27ace42836d81633faf9daf38f6787fff0394ea484bbcd465b57d4dbee3cf8df
+b77d1db287b3a6264c466805be5a4fe85cfbca180699859280f2dd8e2c2c10b5
+7a7d2ac670c6039d41952fbb0e4f99b560ebe1d020e1b96d02403283819c00cc
+529c51f0b0101555e4c58002ba3c6e3c12e3fde1aec94382792e96d9666a2b33
+3dc397b22ecab67ee38a552fec29a1d4ff8719c748"""
+))
+
+#
+# openssl pkcs8 -topk8 -passin pass:TestTest -inform DER -in key.der
+# -outform DER -out keyenc.der -v1 PBE-SHA1-DES
+# hexdump -v -e '32/1 "%02x" "\n"' keyenc.der
+#
+wrapped_enc_keys.append((
+'skip encryption', # pbeWithSHA1AndDES-CBC, only decoding is supported
+-1,
+"",
+"",
+"""
+308201f1301b06092a864886f70d01050a300e04089bacc9cf1e8f734e020208
+00048201d03e502f3ceafe8fd19ab2939576bfdded26d719b2441db1459688f5
+9673218b41ec1f739edf1e460bd927bc28470c87b2d4fc8ea02ba17b47a63c49
+c5c1bee40529dadfd3ef8b4472c730bc136678c78abfb34670ec9d7dcd17ee3f
+892f93f2629e6e0f4b24ecb9f954069bf722f466dece3913bb6abbd2c471d9a5
+c5eea89b14aaccda43d30b0dd0f6eb6e9850d9747aa8aa8414c383ad01c374ee
+26d3552abec9ba22669cc9622ccf2921e3d0c8ecd1a70e861956de0bec6104b5
+b649ac994970c83f8a9e84b14a7dff7843d4ca3dd4af87cea43b5657e15ae0b5
+a940ce5047f006ab3596506600724764f23757205fe374fee04911336d655acc
+03e159ec27789191d1517c4f3f9122f5242d44d25eab8f0658cafb928566ca0e
+8f6589aa0c0ab13ca7a618008ae3eafd4671ee8fe0b562e70b3623b0e2a16eee
+97fd388087d2e03530c9fe7db6e52eccc7c48fd701ede35e08922861a9508d12
+bc8bbf24f0c6bee6e63dbcb489b603d4c4a78ce45bf2eab1d5d10456c42a65a8
+3a606f4e4b9b46eb13b57f2624b651859d3d2d5192b45dbd5a2ead14ff20ca76
+48f321309aa56d8c0c4a192b580821cc6c70c75e6f19d1c5414da898ec4dd39d
+b0eb93d6ba387a80702dfd2db610757ba340f63230
+"""
+))
+
+#
+# openssl pkcs8 -topk8 -passin pass:TestTest -inform DER -in key.der
+# -outform DER -out keyenc.der -v2 aes128
+# hexdump -v -e '32/1 "%02x" "\n"' keyenc.der
+#
+wrapped_enc_keys.append((
+'PBKDF2WithHMAC-SHA1AndAES128-CBC',
+2048,
+"4F66EE5D3BCD531FE6EBF4B4E73016B8", # IV
+"479F25156176C53A", # Salt
+"""
+3082021f304906092a864886f70d01050d303c301b06092a864886f70d01050c
+300e0408479f25156176c53a02020800301d060960864801650304010204104f
+66ee5d3bcd531fe6ebf4b4e73016b8048201d0e33cfa560423f589d097d21533
+3b880a5ebac5b2ac58b4e73b0d787aee7764f034fe34ca1d1bd845c0a7c3316f
+afbfb2129e03dcaf5a5031394206492828dacef1e04639bee5935e0f46114202
+10bc6c37182f4889be11c5d0486c398f4be952e5740f65de9d8edeb275e2b406
+e19bc29ad5ebb97fa536344fc3d84c7e755696f12b810898de4e6f069b8a81c8
+0aab0d45d7d062303aaa4a10c2ce84fdb5a03114039cfe138e38bb15b2ced717
+93549cdad85e730b14d9e2198b663dfdc8d04a4349eb3de59b076ad40b116d4a
+25ed917c576bc7c883c95ef0f1180e28fc9981bea069594c309f1aa1b253ceab
+a2f0313bb1372bcb51a745056be93d77a1f235a762a45e8856512d436b2ca0f7
+dd60fbed394ba28978d2a2b984b028529d0a58d93aba46c6bbd4ac1e4013cbaa
+63b00988bc5f11ccc40141c346762d2b28f64435d4be98ec17c1884985e3807e
+e550db606600993efccf6de0dfc2d2d70b5336a3b018fa415d6bdd59f5777118
+16806b7bc17c4c7e20ad7176ebfa5a1aa3f6bc10f04b77afd443944642ac9cca
+d740e082b4a3bbb8bafdd34a0b3c5f2f3c2aceccccdccd092b78994b845bfa61
+706c3b9df5165ed1dbcbf1244fe41fc9bf993f52f7658e2f87e1baaeacb0f562
+9d905c
+"""
+))
+
+#
+# openssl pkcs8 -topk8 -passin pass:TestTest -inform DER -in key.der
+# -outform DER -out keyenc.der -v2 aes192
+# hexdump -v -e '32/1 "%02x" "\n"' keyenc.der
+#
+wrapped_enc_keys.append((
+'PBKDF2WithHMAC-SHA1AndAES192-CBC',
+2048,
+"5CFC2A4FF7B63201A4A8A5B021148186", # IV
+"D718541C264944CE", # Salt
+"""
+3082021f304906092a864886f70d01050d303c301b06092a864886f70d01050c
+300e0408d718541c264944ce02020800301d060960864801650304011604105c
+fc2a4ff7b63201a4a8a5b021148186048201d08e74aaa21b8bcfb15b9790fe95
+b0e09ddb0f189b6fb1682fdb9f122b804650ddec3c67a1df093a828b3e5fbcc6
+286abbcc5354c482fd796d972e919ca8a5eba1eaa2293af1d648013ddad72106
+75622264dfba55dafdda39e338f058f1bdb9846041ffff803797d3fdf3693135
+8a192729ea8346a7e5e58e925a2e2e4af0818581859e8215d87370eb4194a5ff
+bae900857d4c591dbc651a241865a817eaede9987c9f9ae4f95c0bf930eea88c
+4d7596e535ffb7ca369988aba75027a96b9d0bc9c8b0b75f359067fd145a378b
+02aaa15e9db7a23176224da48a83249005460cc6e429168657f2efa8b1af7537
+d7d7042f2d683e8271b21d591090963eeb57aea6172f88da139e1614d6a7d1a2
+1002d5a7a93d6d21156e2b4777f6fc069287a85a1538c46b7722ccde591ab55c
+630e1ceeb1ac42d1b41f3f654e9da86b5efced43775ea68b2594e50e4005e052
+0fe753c0898120c2c07265367ff157f6538a1e4080d6f9d1ca9eb51939c9574e
+f2e4e1e87c1434affd5808563cddd376776dbbf790c6a40028f311a8b58dafa2
+0970ed34acd6e3e89d063987893b2b9570ddb8cc032b05a723bba9444933ebf3
+c624204be72f4190e0245197d0cb772bec933fd8442445f9a28bd042d5a3a1e9
+9a8a07
+"""
+))
+
+#
+# openssl pkcs8 -topk8 -passin pass:TestTest -inform DER -in key.der
+# -outform DER -out keyenc.der -v2 aes192
+# hexdump -v -e '32/1 "%02x" "\n"' keyenc.der
+#
+wrapped_enc_keys.append((
+'PBKDF2WithHMAC-SHA1AndAES256-CBC',
+2048,
+"323351F94462AC563E053A056252C2C4", # IV
+"02A6CD0D12E727B5", # Salt
+"""
+3082021f304906092a864886f70d01050d303c301b06092a864886f70d01050c
+300e040802a6cd0d12e727b502020800301d060960864801650304012a041032
+3351f94462ac563e053a056252c2c4048201d07f4ef1c7be21aae738a20c5632
+b8bdbbb9083b6e7f68822267b1f481fd27fdafd61a90660de6e4058790e4c912
+bf3f319a7c37e6eb3d956daaa143865020d554bf6215e8d7492359aaeef45d6e
+d85a686ed26c0bf7c18d071d827a86f0b73e1db0c0e7f3d42201544093302a90
+551ad530692468c47ac15c69500b8ca67d4a17b64d15cecc035ae50b768a36cf
+07c395afa091e9e6f86f665455fbdc1b21ad79c0908b73da5de75a9b43508d5d
+44dc97a870cd3cd9f01ca24452e9b11c1b4982946702cfcbfda5b2fcc0203fb5
+0b52a115760bd635c94d4c95ac2c640ee9a04ffaf6ccff5a8d953dd5d88ca478
+c377811c521f2191639c643d657a9e364af88bb7c14a356c2b0b4870a23c2f54
+d41f8157afff731471dccc6058b15e1151bcf84b39b5e622a3a1d65859c912a5
+591b85e034a1f6af664f030a6bfc8c3d20c70f32b54bcf4da9c2da83cef49cf8
+e9a74f0e5d358fe50b88acdce6a9db9a7ad61536212fc5f877ebfc7957b8bda4
+b1582a0f10d515a20ee06cf768db9c977aa6fbdca7540d611ff953012d009dac
+e8abd059f8e8ffea637c9c7721f817aaf0bb23403e26a0ef0ff0e2037da67d41
+af728481f53443551a9bff4cea023164e9622b5441a309e1f4bff98e5bf76677
+8d7cd9
+"""
+))
+
+# hexdump -v -e '32/1 "%02x" "\n"' botan_scrypt.der
+botan_scrypt = """
+3081f1305206092a864886f70d01050d3045302806092b06010401da47040b30
+1b040c316c5c7a847276a838a668280202200002010102010102012030190609
+60864801650304012e040c293e9bcddc0d59d64e060cb604819ab92318063480
+16148081a3123bb092b636ec0cc3b964628e181504c13eaf94987e6fb9f171d4
+9c45baeeb79c1d805d5a762d9bfd6d1995669df60a2cd0174b6d204693964de7
+05bc3fdc3a4ce5db01f30a994c82b0aac786e4f8655138c952f1cf2cc6093f90
+b5e5ca507beb539ff497b7b6370ba7f31f4928d3385dbe8bcd2395813ba1324e
+6795e81a8518aff0f0a9e01396539f937b8b7b08
+"""
+
+# hexdump -v -e '32/1 "%02x" "\n"' botan_pbkdf2.der
+botan_pbkdf2 = """
+3081f3305e06092a864886f70d01050d3051303006092a864886f70d01050c30
+23040cc91c89b368db578d2ec4c32002020fa0020118300c06082a864886f70d
+02090500301d060960864801650304011604102a7147289e7c914a7d8257e4a1
+a2135b048190a648955fc96ecae56dcb4d0ab19edc5b7ef1219c88c7c3b2d0ed
+b21e25d2559447f53e20b90b2f20e72456d943561c4925aad6067a4c720afb3d
+691e14dfffa10ef77898e21d134f19136d35088a7aac508b296fd00d5742ad69
+8c693293b6a591e3660b130d718724d23d696f4da9bf4031475fafb682d7955c
+996363f37032e10ac85afebb7cc1cbfc0e5d4c60a4c2
+"""
+
+def txt2bin(inputs):
+ s = b('').join([b(x) for x in inputs if not (x in '\n\r\t ')])
+ return unhexlify(s)
+
+class Rng:
+ def __init__(self, output):
+ self.output=output
+ self.idx=0
+ def __call__(self, n):
+ output = self.output[self.idx:self.idx+n]
+ self.idx += n
+ return output
+
+class PKCS8_Decrypt(unittest.TestCase):
+
+ def setUp(self):
+ self.oid_key = oid_key
+ self.clear_key = txt2bin(clear_key)
+ self.wrapped_clear_key = txt2bin(wrapped_clear_key)
+ self.wrapped_enc_keys = []
+ for t in wrapped_enc_keys:
+ self.wrapped_enc_keys.append((
+ t[0],
+ t[1],
+ txt2bin(t[2]),
+ txt2bin(t[3]),
+ txt2bin(t[4])
+ ))
+
+ ### NO ENCRYTION
+
+ def test1(self):
+ """Verify unwrapping w/o encryption"""
+ res1, res2, res3 = PKCS8.unwrap(self.wrapped_clear_key)
+ self.assertEqual(res1, self.oid_key)
+ self.assertEqual(res2, self.clear_key)
+
+ def test2(self):
+ """Verify wrapping w/o encryption"""
+ wrapped = PKCS8.wrap(self.clear_key, self.oid_key)
+ res1, res2, res3 = PKCS8.unwrap(wrapped)
+ self.assertEqual(res1, self.oid_key)
+ self.assertEqual(res2, self.clear_key)
+
+ ## ENCRYPTION
+
+ def test3(self):
+ """Verify unwrapping with encryption"""
+
+ for t in self.wrapped_enc_keys:
+ res1, res2, res3 = PKCS8.unwrap(t[4], b"TestTest")
+ self.assertEqual(res1, self.oid_key)
+ self.assertEqual(res2, self.clear_key)
+
+ def test4(self):
+ """Verify wrapping with encryption"""
+
+ for t in self.wrapped_enc_keys:
+ if t[0] == 'skip encryption':
+ continue
+ rng = Rng(t[2]+t[3])
+ params = { 'iteration_count':t[1] }
+ wrapped = PKCS8.wrap(
+ self.clear_key,
+ self.oid_key,
+ b("TestTest"),
+ protection=t[0],
+ prot_params=params,
+ key_params=DerNull(),
+ randfunc=rng)
+ self.assertEqual(wrapped, t[4])
+
+ def test_import_botan_keys(self):
+ botan_scrypt_der = txt2bin(botan_scrypt)
+ key1 = PKCS8.unwrap(botan_scrypt_der,
+ b'your_password')
+ botan_pbkdf2_der = txt2bin(botan_pbkdf2)
+ key2 = PKCS8.unwrap(botan_pbkdf2_der,
+ b'your_password')
+ self.assertEqual(key1, key2)
+
+
+def get_tests(config={}):
+ from Crypto.SelfTest.st_common import list_test_cases
+ listTests = []
+ listTests += list_test_cases(PKCS8_Decrypt)
+ return listTests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/__init__.py
new file mode 100644
index 00000000..c72d7dc3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/__init__.py
@@ -0,0 +1,51 @@
+#
+# SelfTest/Math/__init__.py: Self-test for math module
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""Self-test for Math"""
+
+def get_tests(config={}):
+ tests = []
+ from Crypto.SelfTest.Math import test_Numbers
+ from Crypto.SelfTest.Math import test_Primality
+ from Crypto.SelfTest.Math import test_modexp
+ from Crypto.SelfTest.Math import test_modmult
+ tests += test_Numbers.get_tests(config=config)
+ tests += test_Primality.get_tests(config=config)
+ tests += test_modexp.get_tests(config=config)
+ tests += test_modmult.get_tests(config=config)
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_Numbers.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_Numbers.py
new file mode 100644
index 00000000..a920bf14
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_Numbers.py
@@ -0,0 +1,838 @@
+#
+# SelfTest/Math/test_Numbers.py: Self-test for Numbers module
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""Self-test for Math.Numbers"""
+
+import sys
+import unittest
+
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Util.py3compat import *
+
+from Crypto.Math._IntegerNative import IntegerNative
+
+
+class TestIntegerBase(unittest.TestCase):
+
+ def setUp(self):
+ raise NotImplementedError("To be implemented")
+
+ def Integers(self, *arg):
+ return map(self.Integer, arg)
+
+ def test_init_and_equality(self):
+ Integer = self.Integer
+
+ v1 = Integer(23)
+ v2 = Integer(v1)
+ v3 = Integer(-9)
+ self.assertRaises(ValueError, Integer, 1.0)
+
+ v4 = Integer(10**10)
+ v5 = Integer(-10**10)
+
+ v6 = Integer(0xFFFF)
+ v7 = Integer(0xFFFFFFFF)
+ v8 = Integer(0xFFFFFFFFFFFFFFFF)
+
+ self.assertEqual(v1, v1)
+ self.assertEqual(v1, 23)
+ self.assertEqual(v1, v2)
+ self.assertEqual(v3, -9)
+ self.assertEqual(v4, 10 ** 10)
+ self.assertEqual(v5, -10 ** 10)
+ self.assertEqual(v6, 0xFFFF)
+ self.assertEqual(v7, 0xFFFFFFFF)
+ self.assertEqual(v8, 0xFFFFFFFFFFFFFFFF)
+
+ self.assertFalse(v1 == v4)
+
+ # Init and comparison between Integer's
+ v6 = Integer(v1)
+ self.assertEqual(v1, v6)
+
+ self.assertFalse(Integer(0) == None)
+
+ def test_conversion_to_int(self):
+ v1, v2 = self.Integers(-23, 2 ** 1000)
+ self.assertEqual(int(v1), -23)
+ self.assertEqual(int(v2), 2 ** 1000)
+
+ def test_equality_with_ints(self):
+ v1, v2, v3 = self.Integers(23, -89, 2 ** 1000)
+ self.assertTrue(v1 == 23)
+ self.assertTrue(v2 == -89)
+ self.assertFalse(v1 == 24)
+ self.assertTrue(v3 == 2 ** 1000)
+
+ def test_conversion_to_str(self):
+ v1, v2, v3, v4 = self.Integers(20, 0, -20, 2 ** 1000)
+ self.assertTrue(str(v1) == "20")
+ self.assertTrue(str(v2) == "0")
+ self.assertTrue(str(v3) == "-20")
+ self.assertTrue(str(v4) == "10715086071862673209484250490600018105614048117055336074437503883703510511249361224931983788156958581275946729175531468251871452856923140435984577574698574803934567774824230985421074605062371141877954182153046474983581941267398767559165543946077062914571196477686542167660429831652624386837205668069376")
+
+ def test_repr(self):
+ v1, v2 = self.Integers(-1, 2**80)
+ self.assertEqual(repr(v1), "Integer(-1)")
+ self.assertEqual(repr(v2), "Integer(1208925819614629174706176)")
+
+ def test_conversion_to_bytes(self):
+ Integer = self.Integer
+
+ v0 = Integer(0)
+ self.assertEqual(b"\x00", v0.to_bytes())
+
+ v1 = Integer(0x17)
+ self.assertEqual(b"\x17", v1.to_bytes())
+
+ v2 = Integer(0xFFFE)
+ self.assertEqual(b"\xFF\xFE", v2.to_bytes())
+ self.assertEqual(b"\x00\xFF\xFE", v2.to_bytes(3))
+ self.assertRaises(ValueError, v2.to_bytes, 1)
+
+ self.assertEqual(b"\xFE\xFF", v2.to_bytes(byteorder='little'))
+ self.assertEqual(b"\xFE\xFF\x00", v2.to_bytes(3, byteorder='little'))
+
+ v3 = Integer(0xFF00AABBCCDDEE1122)
+ self.assertEqual(b"\xFF\x00\xAA\xBB\xCC\xDD\xEE\x11\x22", v3.to_bytes())
+ self.assertEqual(b"\x22\x11\xEE\xDD\xCC\xBB\xAA\x00\xFF",
+ v3.to_bytes(byteorder='little'))
+ self.assertEqual(b"\x00\xFF\x00\xAA\xBB\xCC\xDD\xEE\x11\x22",
+ v3.to_bytes(10))
+ self.assertEqual(b"\x22\x11\xEE\xDD\xCC\xBB\xAA\x00\xFF\x00",
+ v3.to_bytes(10, byteorder='little'))
+ self.assertRaises(ValueError, v3.to_bytes, 8)
+
+ v4 = Integer(-90)
+ self.assertRaises(ValueError, v4.to_bytes)
+ self.assertRaises(ValueError, v4.to_bytes, byteorder='bittle')
+
+ def test_conversion_from_bytes(self):
+ Integer = self.Integer
+
+ v1 = Integer.from_bytes(b"\x00")
+ self.assertTrue(isinstance(v1, Integer))
+ self.assertEqual(0, v1)
+
+ v2 = Integer.from_bytes(b"\x00\x01")
+ self.assertEqual(1, v2)
+
+ v3 = Integer.from_bytes(b"\xFF\xFF")
+ self.assertEqual(0xFFFF, v3)
+
+ v4 = Integer.from_bytes(b"\x00\x01", 'big')
+ self.assertEqual(1, v4)
+
+ v5 = Integer.from_bytes(b"\x00\x01", byteorder='big')
+ self.assertEqual(1, v5)
+
+ v6 = Integer.from_bytes(b"\x00\x01", byteorder='little')
+ self.assertEqual(0x0100, v6)
+
+ self.assertRaises(ValueError, Integer.from_bytes, b'\x09', 'bittle')
+
+ def test_inequality(self):
+ # Test Integer!=Integer and Integer!=int
+ v1, v2, v3, v4 = self.Integers(89, 89, 90, -8)
+ self.assertTrue(v1 != v3)
+ self.assertTrue(v1 != 90)
+ self.assertFalse(v1 != v2)
+ self.assertFalse(v1 != 89)
+ self.assertTrue(v1 != v4)
+ self.assertTrue(v4 != v1)
+ self.assertTrue(self.Integer(0) != None)
+
+ def test_less_than(self):
+ # Test IntegerInteger and Integer>int
+ v1, v2, v3, v4, v5 = self.Integers(13, 13, 14, -8, 2 ** 10)
+ self.assertTrue(v3 > v1)
+ self.assertTrue(v3 > 13)
+ self.assertFalse(v1 > v1)
+ self.assertFalse(v1 > v2)
+ self.assertFalse(v1 > 13)
+ self.assertTrue(v1 > v4)
+ self.assertFalse(v4 > v1)
+ self.assertTrue(v5 > v1)
+ self.assertFalse(v1 > v5)
+
+ def test_more_than_or_equal(self):
+ # Test Integer>=Integer and Integer>=int
+ v1, v2, v3, v4 = self.Integers(13, 13, 14, -4)
+ self.assertTrue(v3 >= v1)
+ self.assertTrue(v3 >= 13)
+ self.assertTrue(v1 >= v2)
+ self.assertTrue(v1 >= v1)
+ self.assertTrue(v1 >= 13)
+ self.assertFalse(v4 >= v1)
+
+ def test_bool(self):
+ v1, v2, v3, v4 = self.Integers(0, 10, -9, 2 ** 10)
+ self.assertFalse(v1)
+ self.assertFalse(bool(v1))
+ self.assertTrue(v2)
+ self.assertTrue(bool(v2))
+ self.assertTrue(v3)
+ self.assertTrue(v4)
+
+ def test_is_negative(self):
+ v1, v2, v3, v4, v5 = self.Integers(-3 ** 100, -3, 0, 3, 3**100)
+ self.assertTrue(v1.is_negative())
+ self.assertTrue(v2.is_negative())
+ self.assertFalse(v4.is_negative())
+ self.assertFalse(v5.is_negative())
+
+ def test_addition(self):
+ # Test Integer+Integer and Integer+int
+ v1, v2, v3 = self.Integers(7, 90, -7)
+ self.assertTrue(isinstance(v1 + v2, self.Integer))
+ self.assertEqual(v1 + v2, 97)
+ self.assertEqual(v1 + 90, 97)
+ self.assertEqual(v1 + v3, 0)
+ self.assertEqual(v1 + (-7), 0)
+ self.assertEqual(v1 + 2 ** 10, 2 ** 10 + 7)
+
+ def test_subtraction(self):
+ # Test Integer-Integer and Integer-int
+ v1, v2, v3 = self.Integers(7, 90, -7)
+ self.assertTrue(isinstance(v1 - v2, self.Integer))
+ self.assertEqual(v2 - v1, 83)
+ self.assertEqual(v2 - 7, 83)
+ self.assertEqual(v2 - v3, 97)
+ self.assertEqual(v1 - (-7), 14)
+ self.assertEqual(v1 - 2 ** 10, 7 - 2 ** 10)
+
+ def test_multiplication(self):
+ # Test Integer-Integer and Integer-int
+ v1, v2, v3, v4 = self.Integers(4, 5, -2, 2 ** 10)
+ self.assertTrue(isinstance(v1 * v2, self.Integer))
+ self.assertEqual(v1 * v2, 20)
+ self.assertEqual(v1 * 5, 20)
+ self.assertEqual(v1 * -2, -8)
+ self.assertEqual(v1 * 2 ** 10, 4 * (2 ** 10))
+
+ def test_floor_div(self):
+ v1, v2, v3 = self.Integers(3, 8, 2 ** 80)
+ self.assertTrue(isinstance(v1 // v2, self.Integer))
+ self.assertEqual(v2 // v1, 2)
+ self.assertEqual(v2 // 3, 2)
+ self.assertEqual(v2 // -3, -3)
+ self.assertEqual(v3 // 2 ** 79, 2)
+ self.assertRaises(ZeroDivisionError, lambda: v1 // 0)
+
+ def test_remainder(self):
+ # Test Integer%Integer and Integer%int
+ v1, v2, v3 = self.Integers(23, 5, -4)
+ self.assertTrue(isinstance(v1 % v2, self.Integer))
+ self.assertEqual(v1 % v2, 3)
+ self.assertEqual(v1 % 5, 3)
+ self.assertEqual(v3 % 5, 1)
+ self.assertEqual(v1 % 2 ** 10, 23)
+ self.assertRaises(ZeroDivisionError, lambda: v1 % 0)
+ self.assertRaises(ValueError, lambda: v1 % -6)
+
+ def test_simple_exponentiation(self):
+ v1, v2, v3 = self.Integers(4, 3, -2)
+ self.assertTrue(isinstance(v1 ** v2, self.Integer))
+ self.assertEqual(v1 ** v2, 64)
+ self.assertEqual(pow(v1, v2), 64)
+ self.assertEqual(v1 ** 3, 64)
+ self.assertEqual(pow(v1, 3), 64)
+ self.assertEqual(v3 ** 2, 4)
+ self.assertEqual(v3 ** 3, -8)
+
+ self.assertRaises(ValueError, pow, v1, -3)
+
+ def test_modular_exponentiation(self):
+ v1, v2, v3 = self.Integers(23, 5, 17)
+
+ self.assertTrue(isinstance(pow(v1, v2, v3), self.Integer))
+ self.assertEqual(pow(v1, v2, v3), 7)
+ self.assertEqual(pow(v1, 5, v3), 7)
+ self.assertEqual(pow(v1, v2, 17), 7)
+ self.assertEqual(pow(v1, 5, 17), 7)
+ self.assertEqual(pow(v1, 0, 17), 1)
+ self.assertEqual(pow(v1, 1, 2 ** 80), 23)
+ self.assertEqual(pow(v1, 2 ** 80, 89298), 17689)
+
+ self.assertRaises(ZeroDivisionError, pow, v1, 5, 0)
+ self.assertRaises(ValueError, pow, v1, 5, -4)
+ self.assertRaises(ValueError, pow, v1, -3, 8)
+
+ def test_inplace_exponentiation(self):
+ v1 = self.Integer(4)
+ v1.inplace_pow(2)
+ self.assertEqual(v1, 16)
+
+ v1 = self.Integer(4)
+ v1.inplace_pow(2, 15)
+ self.assertEqual(v1, 1)
+
+ def test_abs(self):
+ v1, v2, v3, v4, v5 = self.Integers(-2 ** 100, -2, 0, 2, 2 ** 100)
+ self.assertEqual(abs(v1), 2 ** 100)
+ self.assertEqual(abs(v2), 2)
+ self.assertEqual(abs(v3), 0)
+ self.assertEqual(abs(v4), 2)
+ self.assertEqual(abs(v5), 2 ** 100)
+
+ def test_sqrt(self):
+ v1, v2, v3, v4 = self.Integers(-2, 0, 49, 10**100)
+
+ self.assertRaises(ValueError, v1.sqrt)
+ self.assertEqual(v2.sqrt(), 0)
+ self.assertEqual(v3.sqrt(), 7)
+ self.assertEqual(v4.sqrt(), 10**50)
+
+ def test_sqrt_module(self):
+
+ # Invalid modulus (non positive)
+ self.assertRaises(ValueError, self.Integer(5).sqrt, 0)
+ self.assertRaises(ValueError, self.Integer(5).sqrt, -1)
+
+ # Simple cases
+ assert self.Integer(0).sqrt(5) == 0
+ assert self.Integer(1).sqrt(5) in (1, 4)
+
+ # Test with all quadratic residues in several fields
+ for p in (11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53):
+ for i in range(0, p):
+ square = i**2 % p
+ res = self.Integer(square).sqrt(p)
+ assert res in (i, p - i)
+
+ # 2 is a non-quadratic reside in Z_11
+ self.assertRaises(ValueError, self.Integer(2).sqrt, 11)
+
+ # 10 is not a prime
+ self.assertRaises(ValueError, self.Integer(4).sqrt, 10)
+
+ # 5 is square residue of 4 and 7
+ assert self.Integer(5 - 11).sqrt(11) in (4, 7)
+ assert self.Integer(5 + 11).sqrt(11) in (4, 7)
+
+ def test_in_place_add(self):
+ v1, v2 = self.Integers(10, 20)
+
+ v1 += v2
+ self.assertEqual(v1, 30)
+ v1 += 10
+ self.assertEqual(v1, 40)
+ v1 += -1
+ self.assertEqual(v1, 39)
+ v1 += 2 ** 1000
+ self.assertEqual(v1, 39 + 2 ** 1000)
+
+ def test_in_place_sub(self):
+ v1, v2 = self.Integers(10, 20)
+
+ v1 -= v2
+ self.assertEqual(v1, -10)
+ v1 -= -100
+ self.assertEqual(v1, 90)
+ v1 -= 90000
+ self.assertEqual(v1, -89910)
+ v1 -= -100000
+ self.assertEqual(v1, 10090)
+
+ def test_in_place_mul(self):
+ v1, v2 = self.Integers(3, 5)
+
+ v1 *= v2
+ self.assertEqual(v1, 15)
+ v1 *= 2
+ self.assertEqual(v1, 30)
+ v1 *= -2
+ self.assertEqual(v1, -60)
+ v1 *= 2 ** 1000
+ self.assertEqual(v1, -60 * (2 ** 1000))
+
+ def test_in_place_modulus(self):
+ v1, v2 = self.Integers(20, 7)
+
+ v1 %= v2
+ self.assertEqual(v1, 6)
+ v1 %= 2 ** 1000
+ self.assertEqual(v1, 6)
+ v1 %= 2
+ self.assertEqual(v1, 0)
+ def t():
+ v3 = self.Integer(9)
+ v3 %= 0
+ self.assertRaises(ZeroDivisionError, t)
+
+ def test_and(self):
+ v1, v2, v3 = self.Integers(0xF4, 0x31, -0xF)
+ self.assertTrue(isinstance(v1 & v2, self.Integer))
+ self.assertEqual(v1 & v2, 0x30)
+ self.assertEqual(v1 & 0x31, 0x30)
+ self.assertEqual(v1 & v3, 0xF0)
+ self.assertEqual(v1 & -0xF, 0xF0)
+ self.assertEqual(v3 & -0xF, -0xF)
+ self.assertEqual(v2 & (2 ** 1000 + 0x31), 0x31)
+
+ def test_or(self):
+ v1, v2, v3 = self.Integers(0x40, 0x82, -0xF)
+ self.assertTrue(isinstance(v1 | v2, self.Integer))
+ self.assertEqual(v1 | v2, 0xC2)
+ self.assertEqual(v1 | 0x82, 0xC2)
+ self.assertEqual(v2 | v3, -0xD)
+ self.assertEqual(v2 | 2 ** 1000, 2 ** 1000 + 0x82)
+
+ def test_right_shift(self):
+ v1, v2, v3 = self.Integers(0x10, 1, -0x10)
+ self.assertEqual(v1 >> 0, v1)
+ self.assertTrue(isinstance(v1 >> v2, self.Integer))
+ self.assertEqual(v1 >> v2, 0x08)
+ self.assertEqual(v1 >> 1, 0x08)
+ self.assertRaises(ValueError, lambda: v1 >> -1)
+ self.assertEqual(v1 >> (2 ** 1000), 0)
+
+ self.assertEqual(v3 >> 1, -0x08)
+ self.assertEqual(v3 >> (2 ** 1000), -1)
+
+ def test_in_place_right_shift(self):
+ v1, v2, v3 = self.Integers(0x10, 1, -0x10)
+ v1 >>= 0
+ self.assertEqual(v1, 0x10)
+ v1 >>= 1
+ self.assertEqual(v1, 0x08)
+ v1 >>= v2
+ self.assertEqual(v1, 0x04)
+ v3 >>= 1
+ self.assertEqual(v3, -0x08)
+ def l():
+ v4 = self.Integer(0x90)
+ v4 >>= -1
+ self.assertRaises(ValueError, l)
+ def m1():
+ v4 = self.Integer(0x90)
+ v4 >>= 2 ** 1000
+ return v4
+ self.assertEqual(0, m1())
+ def m2():
+ v4 = self.Integer(-1)
+ v4 >>= 2 ** 1000
+ return v4
+ self.assertEqual(-1, m2())
+
+ def _test_left_shift(self):
+ v1, v2, v3 = self.Integers(0x10, 1, -0x10)
+ self.assertEqual(v1 << 0, v1)
+ self.assertTrue(isinstance(v1 << v2, self.Integer))
+ self.assertEqual(v1 << v2, 0x20)
+ self.assertEqual(v1 << 1, 0x20)
+ self.assertEqual(v3 << 1, -0x20)
+ self.assertRaises(ValueError, lambda: v1 << -1)
+ self.assertRaises(ValueError, lambda: v1 << (2 ** 1000))
+
+ def test_in_place_left_shift(self):
+ v1, v2, v3 = self.Integers(0x10, 1, -0x10)
+ v1 <<= 0
+ self.assertEqual(v1, 0x10)
+ v1 <<= 1
+ self.assertEqual(v1, 0x20)
+ v1 <<= v2
+ self.assertEqual(v1, 0x40)
+ v3 <<= 1
+ self.assertEqual(v3, -0x20)
+ def l():
+ v4 = self.Integer(0x90)
+ v4 <<= -1
+ self.assertRaises(ValueError, l)
+ def m():
+ v4 = self.Integer(0x90)
+ v4 <<= 2 ** 1000
+ self.assertRaises(ValueError, m)
+
+
+ def test_get_bit(self):
+ v1, v2, v3 = self.Integers(0x102, -3, 1)
+ self.assertEqual(v1.get_bit(0), 0)
+ self.assertEqual(v1.get_bit(1), 1)
+ self.assertEqual(v1.get_bit(v3), 1)
+ self.assertEqual(v1.get_bit(8), 1)
+ self.assertEqual(v1.get_bit(9), 0)
+
+ self.assertRaises(ValueError, v1.get_bit, -1)
+ self.assertEqual(v1.get_bit(2 ** 1000), 0)
+
+ self.assertRaises(ValueError, v2.get_bit, -1)
+ self.assertRaises(ValueError, v2.get_bit, 0)
+ self.assertRaises(ValueError, v2.get_bit, 1)
+ self.assertRaises(ValueError, v2.get_bit, 2 * 1000)
+
+ def test_odd_even(self):
+ v1, v2, v3, v4, v5 = self.Integers(0, 4, 17, -4, -17)
+
+ self.assertTrue(v1.is_even())
+ self.assertTrue(v2.is_even())
+ self.assertFalse(v3.is_even())
+ self.assertTrue(v4.is_even())
+ self.assertFalse(v5.is_even())
+
+ self.assertFalse(v1.is_odd())
+ self.assertFalse(v2.is_odd())
+ self.assertTrue(v3.is_odd())
+ self.assertFalse(v4.is_odd())
+ self.assertTrue(v5.is_odd())
+
+ def test_size_in_bits(self):
+ v1, v2, v3, v4 = self.Integers(0, 1, 0x100, -90)
+ self.assertEqual(v1.size_in_bits(), 1)
+ self.assertEqual(v2.size_in_bits(), 1)
+ self.assertEqual(v3.size_in_bits(), 9)
+ self.assertRaises(ValueError, v4.size_in_bits)
+
+ def test_size_in_bytes(self):
+ v1, v2, v3, v4, v5, v6 = self.Integers(0, 1, 0xFF, 0x1FF, 0x10000, -9)
+ self.assertEqual(v1.size_in_bytes(), 1)
+ self.assertEqual(v2.size_in_bytes(), 1)
+ self.assertEqual(v3.size_in_bytes(), 1)
+ self.assertEqual(v4.size_in_bytes(), 2)
+ self.assertEqual(v5.size_in_bytes(), 3)
+ self.assertRaises(ValueError, v6.size_in_bits)
+
+ def test_perfect_square(self):
+
+ self.assertFalse(self.Integer(-9).is_perfect_square())
+ self.assertTrue(self.Integer(0).is_perfect_square())
+ self.assertTrue(self.Integer(1).is_perfect_square())
+ self.assertFalse(self.Integer(2).is_perfect_square())
+ self.assertFalse(self.Integer(3).is_perfect_square())
+ self.assertTrue(self.Integer(4).is_perfect_square())
+ self.assertTrue(self.Integer(39*39).is_perfect_square())
+ self.assertFalse(self.Integer(39*39+1).is_perfect_square())
+
+ for x in range(100, 1000):
+ self.assertFalse(self.Integer(x**2+1).is_perfect_square())
+ self.assertTrue(self.Integer(x**2).is_perfect_square())
+
+ def test_fail_if_divisible_by(self):
+ v1, v2, v3 = self.Integers(12, -12, 4)
+
+ # No failure expected
+ v1.fail_if_divisible_by(7)
+ v2.fail_if_divisible_by(7)
+ v2.fail_if_divisible_by(2 ** 80)
+
+ # Failure expected
+ self.assertRaises(ValueError, v1.fail_if_divisible_by, 4)
+ self.assertRaises(ValueError, v1.fail_if_divisible_by, v3)
+
+ def test_multiply_accumulate(self):
+ v1, v2, v3 = self.Integers(4, 3, 2)
+ v1.multiply_accumulate(v2, v3)
+ self.assertEqual(v1, 10)
+ v1.multiply_accumulate(v2, 2)
+ self.assertEqual(v1, 16)
+ v1.multiply_accumulate(3, v3)
+ self.assertEqual(v1, 22)
+ v1.multiply_accumulate(1, -2)
+ self.assertEqual(v1, 20)
+ v1.multiply_accumulate(-2, 1)
+ self.assertEqual(v1, 18)
+ v1.multiply_accumulate(1, 2 ** 1000)
+ self.assertEqual(v1, 18 + 2 ** 1000)
+ v1.multiply_accumulate(2 ** 1000, 1)
+ self.assertEqual(v1, 18 + 2 ** 1001)
+
+ def test_set(self):
+ v1, v2 = self.Integers(3, 6)
+ v1.set(v2)
+ self.assertEqual(v1, 6)
+ v1.set(9)
+ self.assertEqual(v1, 9)
+ v1.set(-2)
+ self.assertEqual(v1, -2)
+ v1.set(2 ** 1000)
+ self.assertEqual(v1, 2 ** 1000)
+
+ def test_inverse(self):
+ v1, v2, v3, v4, v5, v6 = self.Integers(2, 5, -3, 0, 723872, 3433)
+
+ self.assertTrue(isinstance(v1.inverse(v2), self.Integer))
+ self.assertEqual(v1.inverse(v2), 3)
+ self.assertEqual(v1.inverse(5), 3)
+ self.assertEqual(v3.inverse(5), 3)
+ self.assertEqual(v5.inverse(92929921), 58610507)
+ self.assertEqual(v6.inverse(9912), 5353)
+
+ self.assertRaises(ValueError, v2.inverse, 10)
+ self.assertRaises(ValueError, v1.inverse, -3)
+ self.assertRaises(ValueError, v4.inverse, 10)
+ self.assertRaises(ZeroDivisionError, v2.inverse, 0)
+
+ def test_inplace_inverse(self):
+ v1, v2 = self.Integers(2, 5)
+
+ v1.inplace_inverse(v2)
+ self.assertEqual(v1, 3)
+
+ def test_gcd(self):
+ v1, v2, v3, v4 = self.Integers(6, 10, 17, -2)
+ self.assertTrue(isinstance(v1.gcd(v2), self.Integer))
+ self.assertEqual(v1.gcd(v2), 2)
+ self.assertEqual(v1.gcd(10), 2)
+ self.assertEqual(v1.gcd(v3), 1)
+ self.assertEqual(v1.gcd(-2), 2)
+ self.assertEqual(v4.gcd(6), 2)
+
+ def test_lcm(self):
+ v1, v2, v3, v4, v5 = self.Integers(6, 10, 17, -2, 0)
+ self.assertTrue(isinstance(v1.lcm(v2), self.Integer))
+ self.assertEqual(v1.lcm(v2), 30)
+ self.assertEqual(v1.lcm(10), 30)
+ self.assertEqual(v1.lcm(v3), 102)
+ self.assertEqual(v1.lcm(-2), 6)
+ self.assertEqual(v4.lcm(6), 6)
+ self.assertEqual(v1.lcm(0), 0)
+ self.assertEqual(v5.lcm(0), 0)
+
+ def test_jacobi_symbol(self):
+
+ data = (
+ (1001, 1, 1),
+ (19, 45, 1),
+ (8, 21, -1),
+ (5, 21, 1),
+ (610, 987, -1),
+ (1001, 9907, -1),
+ (5, 3439601197, -1)
+ )
+
+ js = self.Integer.jacobi_symbol
+
+ # Jacobi symbol is always 1 for k==1 or n==1
+ for k in range(1, 30):
+ self.assertEqual(js(k, 1), 1)
+ for n in range(1, 30, 2):
+ self.assertEqual(js(1, n), 1)
+
+ # Fail if n is not positive odd
+ self.assertRaises(ValueError, js, 6, -2)
+ self.assertRaises(ValueError, js, 6, -1)
+ self.assertRaises(ValueError, js, 6, 0)
+ self.assertRaises(ValueError, js, 0, 0)
+ self.assertRaises(ValueError, js, 6, 2)
+ self.assertRaises(ValueError, js, 6, 4)
+ self.assertRaises(ValueError, js, 6, 6)
+ self.assertRaises(ValueError, js, 6, 8)
+
+ for tv in data:
+ self.assertEqual(js(tv[0], tv[1]), tv[2])
+ self.assertEqual(js(self.Integer(tv[0]), tv[1]), tv[2])
+ self.assertEqual(js(tv[0], self.Integer(tv[1])), tv[2])
+
+ def test_jacobi_symbol_wikipedia(self):
+
+ # Test vectors from https://en.wikipedia.org/wiki/Jacobi_symbol
+ tv = [
+ (3, [(1, 1), (2, -1), (3, 0), (4, 1), (5, -1), (6, 0), (7, 1), (8, -1), (9, 0), (10, 1), (11, -1), (12, 0), (13, 1), (14, -1), (15, 0), (16, 1), (17, -1), (18, 0), (19, 1), (20, -1), (21, 0), (22, 1), (23, -1), (24, 0), (25, 1), (26, -1), (27, 0), (28, 1), (29, -1), (30, 0)]),
+ (5, [(1, 1), (2, -1), (3, -1), (4, 1), (5, 0), (6, 1), (7, -1), (8, -1), (9, 1), (10, 0), (11, 1), (12, -1), (13, -1), (14, 1), (15, 0), (16, 1), (17, -1), (18, -1), (19, 1), (20, 0), (21, 1), (22, -1), (23, -1), (24, 1), (25, 0), (26, 1), (27, -1), (28, -1), (29, 1), (30, 0)]),
+ (7, [(1, 1), (2, 1), (3, -1), (4, 1), (5, -1), (6, -1), (7, 0), (8, 1), (9, 1), (10, -1), (11, 1), (12, -1), (13, -1), (14, 0), (15, 1), (16, 1), (17, -1), (18, 1), (19, -1), (20, -1), (21, 0), (22, 1), (23, 1), (24, -1), (25, 1), (26, -1), (27, -1), (28, 0), (29, 1), (30, 1)]),
+ (9, [(1, 1), (2, 1), (3, 0), (4, 1), (5, 1), (6, 0), (7, 1), (8, 1), (9, 0), (10, 1), (11, 1), (12, 0), (13, 1), (14, 1), (15, 0), (16, 1), (17, 1), (18, 0), (19, 1), (20, 1), (21, 0), (22, 1), (23, 1), (24, 0), (25, 1), (26, 1), (27, 0), (28, 1), (29, 1), (30, 0)]),
+ (11, [(1, 1), (2, -1), (3, 1), (4, 1), (5, 1), (6, -1), (7, -1), (8, -1), (9, 1), (10, -1), (11, 0), (12, 1), (13, -1), (14, 1), (15, 1), (16, 1), (17, -1), (18, -1), (19, -1), (20, 1), (21, -1), (22, 0), (23, 1), (24, -1), (25, 1), (26, 1), (27, 1), (28, -1), (29, -1), (30, -1)]),
+ (13, [(1, 1), (2, -1), (3, 1), (4, 1), (5, -1), (6, -1), (7, -1), (8, -1), (9, 1), (10, 1), (11, -1), (12, 1), (13, 0), (14, 1), (15, -1), (16, 1), (17, 1), (18, -1), (19, -1), (20, -1), (21, -1), (22, 1), (23, 1), (24, -1), (25, 1), (26, 0), (27, 1), (28, -1), (29, 1), (30, 1)]),
+ (15, [(1, 1), (2, 1), (3, 0), (4, 1), (5, 0), (6, 0), (7, -1), (8, 1), (9, 0), (10, 0), (11, -1), (12, 0), (13, -1), (14, -1), (15, 0), (16, 1), (17, 1), (18, 0), (19, 1), (20, 0), (21, 0), (22, -1), (23, 1), (24, 0), (25, 0), (26, -1), (27, 0), (28, -1), (29, -1), (30, 0)]),
+ (17, [(1, 1), (2, 1), (3, -1), (4, 1), (5, -1), (6, -1), (7, -1), (8, 1), (9, 1), (10, -1), (11, -1), (12, -1), (13, 1), (14, -1), (15, 1), (16, 1), (17, 0), (18, 1), (19, 1), (20, -1), (21, 1), (22, -1), (23, -1), (24, -1), (25, 1), (26, 1), (27, -1), (28, -1), (29, -1), (30, 1)]),
+ (19, [(1, 1), (2, -1), (3, -1), (4, 1), (5, 1), (6, 1), (7, 1), (8, -1), (9, 1), (10, -1), (11, 1), (12, -1), (13, -1), (14, -1), (15, -1), (16, 1), (17, 1), (18, -1), (19, 0), (20, 1), (21, -1), (22, -1), (23, 1), (24, 1), (25, 1), (26, 1), (27, -1), (28, 1), (29, -1), (30, 1)]),
+ (21, [(1, 1), (2, -1), (3, 0), (4, 1), (5, 1), (6, 0), (7, 0), (8, -1), (9, 0), (10, -1), (11, -1), (12, 0), (13, -1), (14, 0), (15, 0), (16, 1), (17, 1), (18, 0), (19, -1), (20, 1), (21, 0), (22, 1), (23, -1), (24, 0), (25, 1), (26, 1), (27, 0), (28, 0), (29, -1), (30, 0)]),
+ (23, [(1, 1), (2, 1), (3, 1), (4, 1), (5, -1), (6, 1), (7, -1), (8, 1), (9, 1), (10, -1), (11, -1), (12, 1), (13, 1), (14, -1), (15, -1), (16, 1), (17, -1), (18, 1), (19, -1), (20, -1), (21, -1), (22, -1), (23, 0), (24, 1), (25, 1), (26, 1), (27, 1), (28, -1), (29, 1), (30, -1)]),
+ (25, [(1, 1), (2, 1), (3, 1), (4, 1), (5, 0), (6, 1), (7, 1), (8, 1), (9, 1), (10, 0), (11, 1), (12, 1), (13, 1), (14, 1), (15, 0), (16, 1), (17, 1), (18, 1), (19, 1), (20, 0), (21, 1), (22, 1), (23, 1), (24, 1), (25, 0), (26, 1), (27, 1), (28, 1), (29, 1), (30, 0)]),
+ (27, [(1, 1), (2, -1), (3, 0), (4, 1), (5, -1), (6, 0), (7, 1), (8, -1), (9, 0), (10, 1), (11, -1), (12, 0), (13, 1), (14, -1), (15, 0), (16, 1), (17, -1), (18, 0), (19, 1), (20, -1), (21, 0), (22, 1), (23, -1), (24, 0), (25, 1), (26, -1), (27, 0), (28, 1), (29, -1), (30, 0)]),
+ (29, [(1, 1), (2, -1), (3, -1), (4, 1), (5, 1), (6, 1), (7, 1), (8, -1), (9, 1), (10, -1), (11, -1), (12, -1), (13, 1), (14, -1), (15, -1), (16, 1), (17, -1), (18, -1), (19, -1), (20, 1), (21, -1), (22, 1), (23, 1), (24, 1), (25, 1), (26, -1), (27, -1), (28, 1), (29, 0), (30, 1)]),
+ ]
+
+ js = self.Integer.jacobi_symbol
+
+ for n, kj in tv:
+ for k, j in kj:
+ self.assertEqual(js(k, n), j)
+
+ def test_hex(self):
+ v1, = self.Integers(0x10)
+ self.assertEqual(hex(v1), "0x10")
+
+ def test_mult_modulo_bytes(self):
+ modmult = self.Integer._mult_modulo_bytes
+
+ res = modmult(4, 5, 19)
+ self.assertEqual(res, b'\x01')
+
+ res = modmult(4 - 19, 5, 19)
+ self.assertEqual(res, b'\x01')
+
+ res = modmult(4, 5 - 19, 19)
+ self.assertEqual(res, b'\x01')
+
+ res = modmult(4 + 19, 5, 19)
+ self.assertEqual(res, b'\x01')
+
+ res = modmult(4, 5 + 19, 19)
+ self.assertEqual(res, b'\x01')
+
+ modulus = 2**512 - 1 # 64 bytes
+ t1 = 13**100
+ t2 = 17**100
+ expect = b"\xfa\xb2\x11\x87\xc3(y\x07\xf8\xf1n\xdepq\x0b\xca\xf3\xd3B,\xef\xf2\xfbf\xcc)\x8dZ*\x95\x98r\x96\xa8\xd5\xc3}\xe2q:\xa2'z\xf48\xde%\xef\t\x07\xbc\xc4[C\x8bUE2\x90\xef\x81\xaa:\x08"
+ self.assertEqual(expect, modmult(t1, t2, modulus))
+
+ self.assertRaises(ZeroDivisionError, modmult, 4, 5, 0)
+ self.assertRaises(ValueError, modmult, 4, 5, -1)
+ self.assertRaises(ValueError, modmult, 4, 5, 4)
+
+
+class TestIntegerInt(TestIntegerBase):
+
+ def setUp(self):
+ self.Integer = IntegerNative
+
+
+class testIntegerRandom(unittest.TestCase):
+
+ def test_random_exact_bits(self):
+
+ for _ in range(1000):
+ a = IntegerNative.random(exact_bits=8)
+ self.assertFalse(a < 128)
+ self.assertFalse(a >= 256)
+
+ for bits_value in range(1024, 1024 + 8):
+ a = IntegerNative.random(exact_bits=bits_value)
+ self.assertFalse(a < 2**(bits_value - 1))
+ self.assertFalse(a >= 2**bits_value)
+
+ def test_random_max_bits(self):
+
+ flag = False
+ for _ in range(1000):
+ a = IntegerNative.random(max_bits=8)
+ flag = flag or a < 128
+ self.assertFalse(a>=256)
+ self.assertTrue(flag)
+
+ for bits_value in range(1024, 1024 + 8):
+ a = IntegerNative.random(max_bits=bits_value)
+ self.assertFalse(a >= 2**bits_value)
+
+ def test_random_bits_custom_rng(self):
+
+ class CustomRNG(object):
+ def __init__(self):
+ self.counter = 0
+
+ def __call__(self, size):
+ self.counter += size
+ return bchr(0) * size
+
+ custom_rng = CustomRNG()
+ a = IntegerNative.random(exact_bits=32, randfunc=custom_rng)
+ self.assertEqual(custom_rng.counter, 4)
+
+ def test_random_range(self):
+
+ func = IntegerNative.random_range
+
+ for x in range(200):
+ a = func(min_inclusive=1, max_inclusive=15)
+ self.assertTrue(1 <= a <= 15)
+
+ for x in range(200):
+ a = func(min_inclusive=1, max_exclusive=15)
+ self.assertTrue(1 <= a < 15)
+
+ self.assertRaises(ValueError, func, min_inclusive=1, max_inclusive=2,
+ max_exclusive=3)
+ self.assertRaises(ValueError, func, max_inclusive=2, max_exclusive=3)
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TestIntegerInt)
+
+ try:
+ from Crypto.Math._IntegerGMP import IntegerGMP
+
+ class TestIntegerGMP(TestIntegerBase):
+ def setUp(self):
+ self.Integer = IntegerGMP
+
+ tests += list_test_cases(TestIntegerGMP)
+ except (ImportError, OSError) as e:
+ if sys.platform == "win32":
+ sys.stdout.write("Skipping GMP tests on Windows\n")
+ else:
+ sys.stdout.write("Skipping GMP tests (%s)\n" % str(e) )
+
+ try:
+ from Crypto.Math._IntegerCustom import IntegerCustom
+
+ class TestIntegerCustomModexp(TestIntegerBase):
+ def setUp(self):
+ self.Integer = IntegerCustom
+
+ tests += list_test_cases(TestIntegerCustomModexp)
+ except (ImportError, OSError) as e:
+ sys.stdout.write("Skipping custom modexp tests (%s)\n" % str(e) )
+
+ tests += list_test_cases(testIntegerRandom)
+ return tests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_Primality.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_Primality.py
new file mode 100644
index 00000000..38344f35
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_Primality.py
@@ -0,0 +1,118 @@
+#
+# SelfTest/Math/test_Primality.py: Self-test for Primality module
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""Self-test for Math.Numbers"""
+
+import unittest
+
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Util.py3compat import *
+
+from Crypto.Math.Numbers import Integer
+from Crypto.Math.Primality import (
+ PROBABLY_PRIME, COMPOSITE,
+ miller_rabin_test, lucas_test,
+ test_probable_prime,
+ generate_probable_prime,
+ generate_probable_safe_prime,
+ )
+
+
+class TestPrimality(unittest.TestCase):
+
+ primes = (1, 2, 3, 5, 7, 11, 13, 17, 19, 23, 2**127-1, 175637383534939453397801320455508570374088202376942372758907369518414308188137781042871856139027160010343454418881888953150175357127346872102307696660678617989191485418582475696230580407111841072614783095326672517315988762029036079794994990250662362650625650262324085116467511357592728695033227611029693067539)
+ composites = (0, 4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 7*23, (2**19-1)*(2**67-1), 9746347772161,)
+
+ def test_miller_rabin(self):
+ for prime in self.primes:
+ self.assertEqual(miller_rabin_test(prime, 3), PROBABLY_PRIME)
+ for composite in self.composites:
+ self.assertEqual(miller_rabin_test(composite, 3), COMPOSITE)
+ self.assertRaises(ValueError, miller_rabin_test, -1, 3)
+
+ def test_lucas(self):
+ for prime in self.primes:
+ res = lucas_test(prime)
+ self.assertEqual(res, PROBABLY_PRIME)
+ for composite in self.composites:
+ res = lucas_test(composite)
+ self.assertEqual(res, COMPOSITE)
+ self.assertRaises(ValueError, lucas_test, -1)
+
+ def test_is_prime(self):
+ primes = (170141183460469231731687303715884105727,
+ 19175002942688032928599,
+ 1363005552434666078217421284621279933627102780881053358473,
+ 2 ** 521 - 1)
+ for p in primes:
+ self.assertEqual(test_probable_prime(p), PROBABLY_PRIME)
+
+ not_primes = (
+ 4754868377601046732119933839981363081972014948522510826417784001,
+ 1334733877147062382486934807105197899496002201113849920496510541601,
+ 260849323075371835669784094383812120359260783810157225730623388382401,
+ )
+ for np in not_primes:
+ self.assertEqual(test_probable_prime(np), COMPOSITE)
+
+ from Crypto.Util.number import sieve_base
+ for p in sieve_base[:100]:
+ res = test_probable_prime(p)
+ self.assertEqual(res, PROBABLY_PRIME)
+
+ def test_generate_prime_bit_size(self):
+ p = generate_probable_prime(exact_bits=512)
+ self.assertEqual(p.size_in_bits(), 512)
+
+ def test_generate_prime_filter(self):
+ def ending_with_one(number):
+ return number % 10 == 1
+
+ for x in range(20):
+ q = generate_probable_prime(exact_bits=160,
+ prime_filter=ending_with_one)
+ self.assertEqual(q % 10, 1)
+
+ def test_generate_safe_prime(self):
+ p = generate_probable_safe_prime(exact_bits=161)
+ self.assertEqual(p.size_in_bits(), 161)
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TestPrimality)
+ return tests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_modexp.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_modexp.py
new file mode 100644
index 00000000..b9eb8698
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_modexp.py
@@ -0,0 +1,201 @@
+#
+# SelfTest/Math/test_modexp.py: Self-test for module exponentiation
+#
+# ===================================================================
+#
+# Copyright (c) 2017, Helder Eijs
+# 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.
+# ===================================================================
+
+"""Self-test for the custom module exponentiation"""
+
+import unittest
+
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Util.number import long_to_bytes, bytes_to_long
+
+from Crypto.Util.py3compat import *
+
+from Crypto.Util._raw_api import (load_pycryptodome_raw_lib,
+ create_string_buffer,
+ get_raw_buffer,
+ c_size_t,
+ c_ulonglong)
+
+from Crypto.Hash import SHAKE128
+from Crypto.Math.Numbers import Integer
+from Crypto.Math._IntegerCustom import _raw_montgomery
+
+from Crypto.Random.random import StrongRandom
+
+
+def create_rng(tag):
+ rng = StrongRandom(SHAKE128.new(data=tag))
+ return rng
+
+class ExceptionModulus(ValueError):
+ pass
+
+def monty_pow(base, exp, modulus):
+ max_len = len(long_to_bytes(max(base, exp, modulus)))
+
+ base_b, exp_b, modulus_b = [ long_to_bytes(x, max_len) for x in
+ (base, exp, modulus) ]
+
+ out = create_string_buffer(max_len)
+ error = _raw_montgomery.monty_pow(
+ out,
+ base_b,
+ exp_b,
+ modulus_b,
+ c_size_t(max_len),
+ c_ulonglong(32)
+ )
+
+ if error == 17:
+ raise ExceptionModulus()
+ if error:
+ raise ValueError("monty_pow failed with error: %d" % error)
+
+ result = bytes_to_long(get_raw_buffer(out))
+ return result
+
+exponent1 = 0x2ce0af628901460a419a08ef950d498b9fd6f271a1a52ac293b86fe5c60efe8e8ba93fa1ebe1eb3d614d2e7b328cb60a2591440e163441a190ecf101ceec245f600fffdcf3f5b3a17a7baeacb96a424db1d7ec985e8ec998bb479fecfffed6a75f9a90fc97062fd973303bce855ad7b8d8272a94025e8532be9aabd54a183f303538d2a7e621b4131d59e823a4625f39bd7d518d7784f7c3a8f19061da74974ff42fa1c063dec2db97d461e291a7d6e721708a5229de166c1246363372854e27f3f08ae274bc16bfd205b028a4d81386494433d516dfbb35f495acba5e4e1d1843cb3c3129b6642a85fc7244ce5845fac071c7f622e4ee12ac43fabeeaa0cd01
+modulus1 = 0xd66691b20071be4d66d4b71032b37fa007cfabf579fcb91e50bfc2753b3f0ce7be74e216aef7e26d4ae180bc20d7bd3ea88a6cbf6f87380e613c8979b5b043b200a8ff8856a3b12875e36e98a7569f3852d028e967551000b02c19e9fa52e83115b89309aabb1e1cf1e2cb6369d637d46775ce4523ea31f64ad2794cbc365dd8a35e007ed3b57695877fbf102dbeb8b3212491398e494314e93726926e1383f8abb5889bea954eb8c0ca1c62c8e9d83f41888095c5e645ed6d32515fe0c58c1368cad84694e18da43668c6f43e61d7c9bca633ddcda7aef5b79bc396d4a9f48e2a9abe0836cc455e435305357228e93d25aaed46b952defae0f57339bf26f5a9
+
+
+class TestModExp(unittest.TestCase):
+
+ def test_small(self):
+ self.assertEqual(1, monty_pow(11,12,19))
+
+ def test_large_1(self):
+ base = 0xfffffffffffffffffffffffffffffffffffffffffffffffffff
+ expected = pow(base, exponent1, modulus1)
+ result = monty_pow(base, exponent1, modulus1)
+ self.assertEqual(result, expected)
+
+ def test_zero_exp(self):
+ base = 0xfffffffffffffffffffffffffffffffffffffffffffffffffff
+ result = monty_pow(base, 0, modulus1)
+ self.assertEqual(result, 1)
+
+ def test_zero_base(self):
+ result = monty_pow(0, exponent1, modulus1)
+ self.assertEqual(result, 0)
+
+ def test_zero_modulus(self):
+ base = 0xfffffffffffffffffffffffffffffffffffffffffffffffff
+ self.assertRaises(ExceptionModulus, monty_pow, base, exponent1, 0)
+ self.assertRaises(ExceptionModulus, monty_pow, 0, 0, 0)
+
+ def test_larger_exponent(self):
+ base = modulus1 - 0xFFFFFFF
+ expected = pow(base, modulus1<<64, modulus1)
+ result = monty_pow(base, modulus1<<64, modulus1)
+ self.assertEqual(result, expected)
+
+ def test_even_modulus(self):
+ base = modulus1 >> 4
+ self.assertRaises(ExceptionModulus, monty_pow, base, exponent1, modulus1-1)
+
+ def test_several_lengths(self):
+ prng = SHAKE128.new().update(b('Test'))
+ for length in range(1, 100):
+ modulus2 = Integer.from_bytes(prng.read(length)) | 1
+ base = Integer.from_bytes(prng.read(length)) % modulus2
+ exponent2 = Integer.from_bytes(prng.read(length))
+
+ expected = pow(base, exponent2, modulus2)
+ result = monty_pow(base, exponent2, modulus2)
+ self.assertEqual(result, expected)
+
+ def test_variable_exponent(self):
+ prng = create_rng(b('Test variable exponent'))
+ for i in range(20):
+ for j in range(7):
+ modulus = prng.getrandbits(8*30) | 1
+ base = prng.getrandbits(8*30) % modulus
+ exponent = prng.getrandbits(i*8+j)
+
+ expected = pow(base, exponent, modulus)
+ result = monty_pow(base, exponent, modulus)
+ self.assertEqual(result, expected)
+
+ exponent ^= (1 << (i*8+j)) - 1
+
+ expected = pow(base, exponent, modulus)
+ result = monty_pow(base, exponent, modulus)
+ self.assertEqual(result, expected)
+
+ def test_stress_63(self):
+ prng = create_rng(b('Test 63'))
+ length = 63
+ for _ in range(2000):
+ modulus = prng.getrandbits(8*length) | 1
+ base = prng.getrandbits(8*length) % modulus
+ exponent = prng.getrandbits(8*length)
+
+ expected = pow(base, exponent, modulus)
+ result = monty_pow(base, exponent, modulus)
+ self.assertEqual(result, expected)
+
+ def test_stress_64(self):
+ prng = create_rng(b('Test 64'))
+ length = 64
+ for _ in range(2000):
+ modulus = prng.getrandbits(8*length) | 1
+ base = prng.getrandbits(8*length) % modulus
+ exponent = prng.getrandbits(8*length)
+
+ expected = pow(base, exponent, modulus)
+ result = monty_pow(base, exponent, modulus)
+ self.assertEqual(result, expected)
+
+ def test_stress_65(self):
+ prng = create_rng(b('Test 65'))
+ length = 65
+ for _ in range(2000):
+ modulus = prng.getrandbits(8*length) | 1
+ base = prng.getrandbits(8*length) % modulus
+ exponent = prng.getrandbits(8*length)
+
+ expected = pow(base, exponent, modulus)
+ result = monty_pow(base, exponent, modulus)
+ self.assertEqual(result, expected)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TestModExp)
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_modmult.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_modmult.py
new file mode 100644
index 00000000..66aa3cd1
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Math/test_modmult.py
@@ -0,0 +1,120 @@
+#
+# SelfTest/Math/test_modmult.py: Self-test for custom modular multiplication
+#
+# ===================================================================
+#
+# Copyright (c) 2023, Helder Eijs
+# 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.
+# ===================================================================
+
+"""Self-test for the custom modular multiplication"""
+
+import unittest
+
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Util.number import long_to_bytes, bytes_to_long
+
+from Crypto.Util._raw_api import (create_string_buffer,
+ get_raw_buffer,
+ c_size_t)
+
+from Crypto.Math._IntegerCustom import _raw_montgomery
+
+
+class ExceptionModulus(ValueError):
+ pass
+
+
+def monty_mult(term1, term2, modulus):
+
+ if term1 >= modulus:
+ term1 %= modulus
+ if term2 >= modulus:
+ term2 %= modulus
+
+ modulus_b = long_to_bytes(modulus)
+ numbers_len = len(modulus_b)
+ term1_b = long_to_bytes(term1, numbers_len)
+ term2_b = long_to_bytes(term2, numbers_len)
+
+ out = create_string_buffer(numbers_len)
+ error = _raw_montgomery.monty_multiply(
+ out,
+ term1_b,
+ term2_b,
+ modulus_b,
+ c_size_t(numbers_len)
+ )
+
+ if error == 17:
+ raise ExceptionModulus()
+ if error:
+ raise ValueError("monty_multiply() failed with error: %d" % error)
+
+ return get_raw_buffer(out)
+
+
+modulus1 = 0xd66691b20071be4d66d4b71032b37fa007cfabf579fcb91e50bfc2753b3f0ce7be74e216aef7e26d4ae180bc20d7bd3ea88a6cbf6f87380e613c8979b5b043b200a8ff8856a3b12875e36e98a7569f3852d028e967551000b02c19e9fa52e83115b89309aabb1e1cf1e2cb6369d637d46775ce4523ea31f64ad2794cbc365dd8a35e007ed3b57695877fbf102dbeb8b3212491398e494314e93726926e1383f8abb5889bea954eb8c0ca1c62c8e9d83f41888095c5e645ed6d32515fe0c58c1368cad84694e18da43668c6f43e61d7c9bca633ddcda7aef5b79bc396d4a9f48e2a9abe0836cc455e435305357228e93d25aaed46b952defae0f57339bf26f5a9
+
+
+class TestModMultiply(unittest.TestCase):
+
+ def test_small(self):
+ self.assertEqual(b"\x01", monty_mult(5, 6, 29))
+
+ def test_large(self):
+ numbers_len = (modulus1.bit_length() + 7) // 8
+
+ t1 = modulus1 // 2
+ t2 = modulus1 - 90
+ expect = b'\x00' * (numbers_len - 1) + b'\x2d'
+ self.assertEqual(expect, monty_mult(t1, t2, modulus1))
+
+ def test_zero_term(self):
+ numbers_len = (modulus1.bit_length() + 7) // 8
+ expect = b'\x00' * numbers_len
+ self.assertEqual(expect, monty_mult(0x100, 0, modulus1))
+ self.assertEqual(expect, monty_mult(0, 0x100, modulus1))
+
+ def test_larger_term(self):
+ t1 = 2**2047
+ expect_int = 0x8edf4071f78e3d7ba622cdbbbef74612e301d69186776ae6bf87ff38c320d9aebaa64889c2f67de2324e6bccd2b10ad89e91fd21ba4bb523904d033eff5e70e62f01a84f41fa90a4f248ef249b82e1d2729253fdfc2a3b5b740198123df8bfbf7057d03e15244ad5f26eb9a099763b5c5972121ec076b0bf899f59bd95f7cc129abddccf24217bce52ca0f3a44c9ccc504765dbb89734205f3ae6a8cc560494a60ea84b27d8e00fa24bdd5b4f1d4232edb61e47d3d984c1fa50a3820a2e580fbc3fc8bc11e99df53b9efadf5a40ac75d384e400905aa6f1d88950cd53b1c54dc2222115ad84a27260fa4d978155c1434c551de1ee7361a17a2f79d4388f78a5d
+ res = bytes_to_long(monty_mult(t1, t1, modulus1))
+ self.assertEqual(res, expect_int)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TestModMultiply)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/__init__.py
new file mode 100644
index 00000000..c20cf2be
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/__init__.py
@@ -0,0 +1,45 @@
+# SelfTest/Protocol/__init__.py: Self-tests for Crypto.Protocol
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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
+
+"""Self-test for Crypto.Protocol"""
+
+def get_tests(config={}):
+ tests = []
+ from Crypto.SelfTest.Protocol import test_rfc1751; tests += test_rfc1751.get_tests(config=config)
+ from Crypto.SelfTest.Protocol import test_KDF; tests += test_KDF.get_tests(config=config)
+ from Crypto.SelfTest.Protocol import test_ecdh; tests += test_ecdh.get_tests(config=config)
+
+ from Crypto.SelfTest.Protocol import test_SecretSharing
+ tests += test_SecretSharing.get_tests(config=config)
+
+ if sys.version_info >= (3, 9):
+ from Crypto.SelfTest.Protocol import test_HPKE
+ tests += test_HPKE.get_tests(config=config)
+
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_HPKE.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_HPKE.py
new file mode 100644
index 00000000..ee6ae94a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_HPKE.py
@@ -0,0 +1,491 @@
+import os
+import json
+import unittest
+from binascii import unhexlify
+
+from Crypto.Protocol import HPKE
+from Crypto.Protocol.HPKE import DeserializeError
+
+from Crypto.PublicKey import ECC
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Protocol import DH
+from Crypto.Hash import SHA256, SHA384, SHA512
+
+
+class HPKE_Tests(unittest.TestCase):
+
+ key1 = ECC.generate(curve='p256')
+ key2 = ECC.generate(curve='p256')
+
+ # name, size of enc
+ curves = {
+ 'p256': 65,
+ 'p384': 97,
+ 'p521': 133,
+ 'curve25519': 32,
+ 'curve448': 56,
+ }
+
+ def round_trip(self, curve, aead_id):
+ key1 = ECC.generate(curve=curve)
+ aead_id = aead_id
+
+ encryptor = HPKE.new(receiver_key=key1.public_key(),
+ aead_id=aead_id)
+ self.assertEqual(len(encryptor.enc), self.curves[curve])
+
+ # First message
+ ct = encryptor.seal(b'ABC', auth_data=b'DEF')
+
+ decryptor = HPKE.new(receiver_key=key1,
+ aead_id=aead_id,
+ enc=encryptor.enc)
+
+ pt = decryptor.unseal(ct, auth_data=b'DEF')
+ self.assertEqual(b'ABC', pt)
+
+ # Second message
+ ct2 = encryptor.seal(b'GHI')
+ pt2 = decryptor.unseal(ct2)
+ self.assertEqual(b'GHI', pt2)
+
+ def test_round_trip(self):
+ for curve in self.curves.keys():
+ for aead_id in HPKE.AEAD:
+ self.round_trip(curve, aead_id)
+
+ def test_psk(self):
+ aead_id = HPKE.AEAD.AES128_GCM
+ HPKE.new(receiver_key=self.key1.public_key(),
+ aead_id=aead_id,
+ psk=(b'a', b'c' * 32))
+
+ def test_info(self):
+ aead_id = HPKE.AEAD.AES128_GCM
+ HPKE.new(receiver_key=self.key1.public_key(),
+ aead_id=aead_id,
+ info=b'baba')
+
+ def test_neg_unsupported_curve(self):
+ key3 = ECC.generate(curve='p224')
+ with self.assertRaises(ValueError) as cm:
+ HPKE.new(receiver_key=key3.public_key(),
+ aead_id=HPKE.AEAD.AES128_GCM)
+ self.assertIn("Unsupported curve", str(cm.exception))
+
+ def test_neg_too_many_private_keys(self):
+ with self.assertRaises(ValueError) as cm:
+ HPKE.new(receiver_key=self.key1,
+ sender_key=self.key2,
+ aead_id=HPKE.AEAD.AES128_GCM)
+ self.assertIn("Exactly 1 private key", str(cm.exception))
+
+ def test_neg_curve_mismatch(self):
+ key3 = ECC.generate(curve='p384')
+ with self.assertRaises(ValueError) as cm:
+ HPKE.new(receiver_key=self.key1.public_key(),
+ sender_key=key3,
+ aead_id=HPKE.AEAD.AES128_GCM)
+ self.assertIn("but recipient key", str(cm.exception))
+
+ def test_neg_psk(self):
+ with self.assertRaises(ValueError) as cm:
+ HPKE.new(receiver_key=self.key1.public_key(),
+ psk=(b'', b'G' * 32),
+ aead_id=HPKE.AEAD.AES128_GCM)
+
+ with self.assertRaises(ValueError) as cm:
+ HPKE.new(receiver_key=self.key1.public_key(),
+ psk=(b'JJJ', b''),
+ aead_id=HPKE.AEAD.AES128_GCM)
+
+ with self.assertRaises(ValueError) as cm:
+ HPKE.new(receiver_key=self.key1.public_key(),
+ psk=(b'JJJ', b'Y' * 31),
+ aead_id=HPKE.AEAD.AES128_GCM)
+ self.assertIn("at least 32", str(cm.exception))
+
+ def test_neg_wrong_enc(self):
+ wrong_enc = b'\xFF' + b'8' * 64
+ with self.assertRaises(DeserializeError):
+ HPKE.new(receiver_key=self.key1,
+ aead_id=HPKE.AEAD.AES128_GCM,
+ enc=wrong_enc)
+
+ with self.assertRaises(ValueError) as cm:
+ HPKE.new(receiver_key=self.key1.public_key(),
+ enc=self.key1.public_key().export_key(format='raw'),
+ aead_id=HPKE.AEAD.AES128_GCM)
+ self.assertIn("'enc' cannot be an input", str(cm.exception))
+
+ with self.assertRaises(ValueError) as cm:
+ HPKE.new(receiver_key=self.key1,
+ aead_id=HPKE.AEAD.AES128_GCM)
+ self.assertIn("'enc' required", str(cm.exception))
+
+ def test_neg_unseal_wrong_ct(self):
+ decryptor = HPKE.new(receiver_key=self.key1,
+ aead_id=HPKE.AEAD.CHACHA20_POLY1305,
+ enc=self.key2.public_key().export_key(format='raw'))
+
+ with self.assertRaises(ValueError):
+ decryptor.unseal(b'XYZ' * 20)
+
+ def test_neg_unseal_no_auth_data(self):
+ aead_id = HPKE.AEAD.CHACHA20_POLY1305
+
+ encryptor = HPKE.new(receiver_key=self.key1.public_key(),
+ aead_id=aead_id)
+
+ ct = encryptor.seal(b'ABC', auth_data=b'DEF')
+
+ decryptor = HPKE.new(receiver_key=self.key1,
+ aead_id=aead_id,
+ enc=encryptor.enc)
+
+ with self.assertRaises(ValueError):
+ decryptor.unseal(ct)
+
+ def test_x25519_mode_0(self):
+ # RFC x9180, A.1.1.1, seq 0 and 1
+
+ keyR_hex = "4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8"
+ keyR = DH.import_x25519_private_key(bytes.fromhex(keyR_hex))
+
+ pt_hex = "4265617574792069732074727574682c20747275746820626561757479"
+ pt = bytes.fromhex(pt_hex)
+
+ ct0_hex = "f938558b5d72f1a23810b4be2ab4f84331acc02fc97babc53a52ae8218a355a96d8770ac83d07bea87e13c512a"
+ ct0 = bytes.fromhex(ct0_hex)
+
+ enc_hex = "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431"
+ enc = bytes.fromhex(enc_hex)
+
+ aad0_hex = "436f756e742d30"
+ aad0 = bytes.fromhex(aad0_hex)
+
+ aad1_hex = "436f756e742d31"
+ aad1 = bytes.fromhex(aad1_hex)
+
+ info_hex = "4f6465206f6e2061204772656369616e2055726e"
+ info = bytes.fromhex(info_hex)
+
+ ct1_hex = "af2d7e9ac9ae7e270f46ba1f975be53c09f8d875bdc8535458c2494e8a6eab251c03d0c22a56b8ca42c2063b84"
+ ct1 = bytes.fromhex(ct1_hex)
+
+ aead_id = HPKE.AEAD.AES128_GCM
+
+ decryptor = HPKE.new(receiver_key=keyR,
+ aead_id=aead_id,
+ info=info,
+ enc=enc)
+
+ pt_X0 = decryptor.unseal(ct0, aad0)
+ self.assertEqual(pt_X0, pt)
+
+ pt_X1 = decryptor.unseal(ct1, aad1)
+ self.assertEqual(pt_X1, pt)
+
+ def test_x25519_mode_1(self):
+ # RFC x9180, A.1.2.1, seq 0 and 1
+
+ keyR_hex = "c5eb01eb457fe6c6f57577c5413b931550a162c71a03ac8d196babbd4e5ce0fd"
+ keyR = DH.import_x25519_private_key(bytes.fromhex(keyR_hex))
+
+ psk_id_hex = "456e6e796e20447572696e206172616e204d6f726961"
+ psk_id = bytes.fromhex(psk_id_hex)
+
+ psk_hex = "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+ psk = bytes.fromhex(psk_hex)
+
+ pt_hex = "4265617574792069732074727574682c20747275746820626561757479"
+ pt = bytes.fromhex(pt_hex)
+
+ ct0_hex = "e52c6fed7f758d0cf7145689f21bc1be6ec9ea097fef4e959440012f4feb73fb611b946199e681f4cfc34db8ea"
+ ct0 = bytes.fromhex(ct0_hex)
+
+ enc_hex = "0ad0950d9fb9588e59690b74f1237ecdf1d775cd60be2eca57af5a4b0471c91b"
+ enc = bytes.fromhex(enc_hex)
+
+ aad0_hex = "436f756e742d30"
+ aad0 = bytes.fromhex(aad0_hex)
+
+ aad1_hex = "436f756e742d31"
+ aad1 = bytes.fromhex(aad1_hex)
+
+ info_hex = "4f6465206f6e2061204772656369616e2055726e"
+ info = bytes.fromhex(info_hex)
+
+ ct1_hex = "49f3b19b28a9ea9f43e8c71204c00d4a490ee7f61387b6719db765e948123b45b61633ef059ba22cd62437c8ba"
+ ct1 = bytes.fromhex(ct1_hex)
+
+ aead_id = HPKE.AEAD.AES128_GCM
+
+ decryptor = HPKE.new(receiver_key=keyR,
+ aead_id=aead_id,
+ info=info,
+ psk=(psk_id, psk),
+ enc=enc)
+
+ pt_X0 = decryptor.unseal(ct0, aad0)
+ self.assertEqual(pt_X0, pt)
+
+ pt_X1 = decryptor.unseal(ct1, aad1)
+ self.assertEqual(pt_X1, pt)
+
+ def test_x25519_mode_2(self):
+ # RFC x9180, A.1.3.1, seq 0 and 1
+
+ keyR_hex = "fdea67cf831f1ca98d8e27b1f6abeb5b7745e9d35348b80fa407ff6958f9137e"
+ keyR = DH.import_x25519_private_key(bytes.fromhex(keyR_hex))
+
+ keyS_hex = "dc4a146313cce60a278a5323d321f051c5707e9c45ba21a3479fecdf76fc69dd"
+ keyS = DH.import_x25519_private_key(bytes.fromhex(keyS_hex))
+
+ pt_hex = "4265617574792069732074727574682c20747275746820626561757479"
+ pt = bytes.fromhex(pt_hex)
+
+ ct0_hex = "5fd92cc9d46dbf8943e72a07e42f363ed5f721212cd90bcfd072bfd9f44e06b80fd17824947496e21b680c141b"
+ ct0 = bytes.fromhex(ct0_hex)
+
+ enc_hex = "23fb952571a14a25e3d678140cd0e5eb47a0961bb18afcf85896e5453c312e76"
+ enc = bytes.fromhex(enc_hex)
+
+ aad0_hex = "436f756e742d30"
+ aad0 = bytes.fromhex(aad0_hex)
+
+ aad1_hex = "436f756e742d31"
+ aad1 = bytes.fromhex(aad1_hex)
+
+ info_hex = "4f6465206f6e2061204772656369616e2055726e"
+ info = bytes.fromhex(info_hex)
+
+ ct1_hex = "d3736bb256c19bfa93d79e8f80b7971262cb7c887e35c26370cfed62254369a1b52e3d505b79dd699f002bc8ed"
+ ct1 = bytes.fromhex(ct1_hex)
+
+ aead_id = HPKE.AEAD.AES128_GCM
+
+ decryptor = HPKE.new(receiver_key=keyR,
+ sender_key=keyS.public_key(),
+ aead_id=aead_id,
+ info=info,
+ enc=enc)
+
+ pt_X0 = decryptor.unseal(ct0, aad0)
+ self.assertEqual(pt_X0, pt)
+
+ pt_X1 = decryptor.unseal(ct1, aad1)
+ self.assertEqual(pt_X1, pt)
+
+ def test_x25519_mode_3(self):
+ # RFC x9180, A.1.4.1, seq 0 and 1
+
+ keyR_hex = "cb29a95649dc5656c2d054c1aa0d3df0493155e9d5da6d7e344ed8b6a64a9423"
+ keyR = DH.import_x25519_private_key(bytes.fromhex(keyR_hex))
+
+ keyS_hex = "fc1c87d2f3832adb178b431fce2ac77c7ca2fd680f3406c77b5ecdf818b119f4"
+ keyS = DH.import_x25519_private_key(bytes.fromhex(keyS_hex))
+
+ psk_id_hex = "456e6e796e20447572696e206172616e204d6f726961"
+ psk_id = bytes.fromhex(psk_id_hex)
+
+ psk_hex = "0247fd33b913760fa1fa51e1892d9f307fbe65eb171e8132c2af18555a738b82"
+ psk = bytes.fromhex(psk_hex)
+
+ pt_hex = "4265617574792069732074727574682c20747275746820626561757479"
+ pt = bytes.fromhex(pt_hex)
+
+ ct0_hex = "a84c64df1e11d8fd11450039d4fe64ff0c8a99fca0bd72c2d4c3e0400bc14a40f27e45e141a24001697737533e"
+ ct0 = bytes.fromhex(ct0_hex)
+
+ enc_hex = "820818d3c23993492cc5623ab437a48a0a7ca3e9639c140fe1e33811eb844b7c"
+ enc = bytes.fromhex(enc_hex)
+
+ aad0_hex = "436f756e742d30"
+ aad0 = bytes.fromhex(aad0_hex)
+
+ aad1_hex = "436f756e742d31"
+ aad1 = bytes.fromhex(aad1_hex)
+
+ info_hex = "4f6465206f6e2061204772656369616e2055726e"
+ info = bytes.fromhex(info_hex)
+
+ ct1_hex = "4d19303b848f424fc3c3beca249b2c6de0a34083b8e909b6aa4c3688505c05ffe0c8f57a0a4c5ab9da127435d9"
+ ct1 = bytes.fromhex(ct1_hex)
+
+ aead_id = HPKE.AEAD.AES128_GCM
+
+ decryptor = HPKE.new(receiver_key=keyR,
+ sender_key=keyS.public_key(),
+ aead_id=aead_id,
+ psk=(psk_id, psk),
+ info=info,
+ enc=enc)
+
+ pt_X0 = decryptor.unseal(ct0, aad0)
+ self.assertEqual(pt_X0, pt)
+
+ pt_X1 = decryptor.unseal(ct1, aad1)
+ self.assertEqual(pt_X1, pt)
+
+
+class HPKE_TestVectors(unittest.TestCase):
+
+ def setUp(self):
+ self.vectors = []
+ try:
+ import pycryptodome_test_vectors # type: ignore
+ init_dir = os.path.dirname(pycryptodome_test_vectors.__file__)
+ full_file_name = os.path.join(init_dir, "Protocol", "wycheproof", "HPKE-test-vectors.json")
+ with open(full_file_name, "r") as f:
+ self.vectors = json.load(f)
+ except (FileNotFoundError, ImportError):
+ print("\nWarning: skipping extended tests for HPKE (install pycryptodome-test-vectors)")
+
+ def import_private_key(self, key_hex, kem_id):
+ key_bin = unhexlify(key_hex)
+ if kem_id == 0x0010:
+ return ECC.construct(curve='p256', d=int.from_bytes(key_bin,
+ byteorder="big"))
+ elif kem_id == 0x0011:
+ return ECC.construct(curve='p384', d=int.from_bytes(key_bin,
+ byteorder="big"))
+ elif kem_id == 0x0012:
+ return ECC.construct(curve='p521', d=int.from_bytes(key_bin,
+ byteorder="big"))
+ elif kem_id == 0x0020:
+ return DH.import_x25519_private_key(key_bin)
+ elif kem_id == 0x0021:
+ return DH.import_x448_private_key(key_bin)
+
+ def test_hpke_encap(self):
+ """Test HPKE encapsulation using test vectors."""
+
+ if not self.vectors:
+ self.skipTest("No test vectors available")
+
+ for idx, vector in enumerate(self.vectors):
+
+ kem_id = vector["kem_id"]
+ kdf_id = vector["kdf_id"]
+ aead_id = vector["aead_id"]
+
+ # No export-only pseudo-cipher
+ if aead_id == 0xffff:
+ continue
+
+ # We support only one KDF per curve
+ supported_combi = {
+ (0x10, 0x1): SHA256,
+ (0x11, 0x2): SHA384,
+ (0x12, 0x3): SHA512,
+ (0x20, 0x1): SHA256,
+ (0x21, 0x3): SHA512,
+ }
+ hashmod = supported_combi.get((kem_id, kdf_id))
+ if hashmod is None:
+ continue
+
+ with self.subTest(idx=idx, kem_id=kem_id, aead_id=aead_id):
+
+ receiver_pub = self.import_private_key(vector["skRm"],
+ kem_id).public_key()
+
+ sender_priv = None
+ if "skSm" in vector:
+ sender_priv = self.import_private_key(vector["skSm"],
+ kem_id)
+
+ encap_key = self.import_private_key(vector["skEm"], kem_id)
+
+ shared_secret, enc = HPKE.HPKE_Cipher._encap(receiver_pub,
+ kem_id,
+ hashmod,
+ sender_priv,
+ encap_key)
+ self.assertEqual(enc.hex(), vector["enc"])
+ self.assertEqual(shared_secret,
+ unhexlify(vector["shared_secret"]))
+
+ print(".", end="", flush=True)
+
+ def test_hpke_unseal(self):
+ """Test HPKE encryption and decryption using test vectors."""
+
+ if not self.vectors:
+ self.skipTest("No test vectors available")
+
+ for idx, vector in enumerate(self.vectors):
+
+ kem_id = vector["kem_id"]
+ kdf_id = vector["kdf_id"]
+ aead_id = vector["aead_id"]
+
+ # No export-only pseudo-cipher
+ if aead_id == 0xffff:
+ continue
+
+ # We support only one KDF per curve
+ supported_combi = (
+ (0x10, 0x1),
+ (0x11, 0x2),
+ (0x12, 0x3),
+ (0x20, 0x1),
+ (0x21, 0x3),
+ )
+ if (kem_id, kdf_id) not in supported_combi:
+ continue
+
+ with self.subTest(idx=idx, kem_id=kem_id, aead_id=aead_id):
+
+ receiver_priv = self.import_private_key(vector["skRm"],
+ kem_id)
+
+ sender_pub = None
+ if "skSm" in vector:
+ sender_priv = self.import_private_key(vector["skSm"],
+ kem_id)
+ sender_pub = sender_priv.public_key()
+
+ encap_key = unhexlify(vector["enc"])
+
+ psk = None
+ if "psk_id" in vector:
+ psk = unhexlify(vector["psk_id"]), unhexlify(vector["psk"])
+
+ receiver_hpke = HPKE.new(receiver_key=receiver_priv,
+ aead_id=HPKE.AEAD(aead_id),
+ enc=encap_key,
+ sender_key=sender_pub,
+ psk=psk,
+ info=unhexlify(vector["info"]))
+
+ for encryption in vector['encryptions']:
+
+ plaintext = unhexlify(encryption["pt"])
+ ciphertext = unhexlify(encryption["ct"])
+ aad = unhexlify(encryption["aad"])
+
+ # Decrypt (unseal)
+ decrypted = receiver_hpke.unseal(ciphertext, aad)
+ self.assertEqual(decrypted, plaintext, "Decryption failed")
+
+ print(".", end="", flush=True)
+
+
+def get_tests(config={}):
+
+ tests = []
+ tests += list_test_cases(HPKE_Tests)
+
+ if config.get('slow_tests'):
+ tests += list_test_cases(HPKE_TestVectors)
+
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_KDF.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_KDF.py
new file mode 100644
index 00000000..1b2aa54c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_KDF.py
@@ -0,0 +1,809 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Protocol/test_KDF.py: Self-test for key derivation functions
+#
+# ===================================================================
+# 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 re
+import unittest
+from binascii import unhexlify
+
+from Crypto.Util.py3compat import b, bchr
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors, load_test_vectors_wycheproof
+from Crypto.Hash import SHA1, HMAC, SHA256, MD5, SHA224, SHA384, SHA512
+from Crypto.Cipher import AES, DES3
+
+from Crypto.Protocol.KDF import (PBKDF1, PBKDF2, _S2V, HKDF, scrypt,
+ bcrypt, bcrypt_check,
+ SP800_108_Counter)
+
+from Crypto.Protocol.KDF import _bcrypt_decode
+
+
+def t2b(t):
+ if t is None:
+ return None
+ t2 = t.replace(" ", "").replace("\n", "")
+ return unhexlify(b(t2))
+
+
+class TestVector(object):
+ pass
+
+
+class PBKDF1_Tests(unittest.TestCase):
+
+ # List of tuples with test data.
+ # Each tuple is made up by:
+ # Item #0: a pass phrase
+ # Item #1: salt (8 bytes encoded in hex)
+ # Item #2: output key length
+ # Item #3: iterations to use
+ # Item #4: expected result (encoded in hex)
+ _testData = (
+ # From http://www.di-mgt.com.au/cryptoKDFs.html#examplespbkdf
+ ("password", "78578E5A5D63CB06", 16, 1000, "DC19847E05C64D2FAF10EBFB4A3D2A20"),
+ )
+
+ def test1(self):
+ v = self._testData[0]
+ res = PBKDF1(v[0], t2b(v[1]), v[2], v[3], SHA1)
+ self.assertEqual(res, t2b(v[4]))
+
+
+class PBKDF2_Tests(unittest.TestCase):
+
+ # List of tuples with test data.
+ # Each tuple is made up by:
+ # Item #0: a pass phrase
+ # Item #1: salt (encoded in hex)
+ # Item #2: output key length
+ # Item #3: iterations to use
+ # Item #4: hash module
+ # Item #5: expected result (encoded in hex)
+ _testData = (
+ # From http://www.di-mgt.com.au/cryptoKDFs.html#examplespbkdf
+ ("password","78578E5A5D63CB06",24,2048, SHA1, "BFDE6BE94DF7E11DD409BCE20A0255EC327CB936FFE93643"),
+ # From RFC 6050
+ ("password","73616c74", 20, 1, SHA1, "0c60c80f961f0e71f3a9b524af6012062fe037a6"),
+ ("password","73616c74", 20, 2, SHA1, "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957"),
+ ("password","73616c74", 20, 4096, SHA1, "4b007901b765489abead49d926f721d065a429c1"),
+ ("passwordPASSWORDpassword","73616c7453414c5473616c7453414c5473616c7453414c5473616c7453414c5473616c74",
+ 25, 4096, SHA1, "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038"),
+ ( 'pass\x00word',"7361006c74",16,4096, SHA1, "56fa6aa75548099dcc37d7f03425e0c3"),
+ # From draft-josefsson-scrypt-kdf-01, Chapter 10
+ ( 'passwd', '73616c74', 64, 1, SHA256, "55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783"),
+ ( 'Password', '4e61436c', 64, 80000, SHA256, "4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d"),
+ )
+
+ def test1(self):
+ # Test only for HMAC-SHA1 as PRF
+
+ def prf_SHA1(p,s):
+ return HMAC.new(p,s,SHA1).digest()
+
+ def prf_SHA256(p,s):
+ return HMAC.new(p,s,SHA256).digest()
+
+ for i in range(len(self._testData)):
+ v = self._testData[i]
+ password = v[0]
+ salt = t2b(v[1])
+ out_len = v[2]
+ iters = v[3]
+ hash_mod = v[4]
+ expected = t2b(v[5])
+
+ if hash_mod is SHA1:
+ res = PBKDF2(password, salt, out_len, iters)
+ self.assertEqual(res, expected)
+
+ res = PBKDF2(password, salt, out_len, iters, prf_SHA1)
+ self.assertEqual(res, expected)
+ else:
+ res = PBKDF2(password, salt, out_len, iters, prf_SHA256)
+ self.assertEqual(res, expected)
+
+ def test2(self):
+ # Verify that prf and hmac_hash_module are mutual exclusive
+ def prf_SHA1(p,s):
+ return HMAC.new(p,s,SHA1).digest()
+
+ self.assertRaises(ValueError, PBKDF2, b("xxx"), b("yyy"), 16, 100,
+ prf=prf_SHA1, hmac_hash_module=SHA1)
+
+ def test3(self):
+ # Verify that hmac_hash_module works like prf
+
+ password = b("xxx")
+ salt = b("yyy")
+
+ for hashmod in (MD5, SHA1, SHA224, SHA256, SHA384, SHA512):
+
+ pr1 = PBKDF2(password, salt, 16, 100,
+ prf=lambda p, s: HMAC.new(p,s,hashmod).digest())
+ pr2 = PBKDF2(password, salt, 16, 100, hmac_hash_module=hashmod)
+
+ self.assertEqual(pr1, pr2)
+
+ def test4(self):
+ # Verify that PBKDF2 can take bytes or strings as password or salt
+ k1 = PBKDF2("xxx", b("yyy"), 16, 10)
+ k2 = PBKDF2(b("xxx"), b("yyy"), 16, 10)
+ self.assertEqual(k1, k2)
+
+ k1 = PBKDF2(b("xxx"), "yyy", 16, 10)
+ k2 = PBKDF2(b("xxx"), b("yyy"), 16, 10)
+ self.assertEqual(k1, k2)
+
+
+class S2V_Tests(unittest.TestCase):
+
+ # Sequence of test vectors.
+ # Each test vector is made up by:
+ # Item #0: a tuple of strings
+ # Item #1: an AES key
+ # Item #2: the result
+ # Item #3: the cipher module S2V is based on
+ # Everything is hex encoded
+ _testData = [
+
+ # RFC5297, A.1
+ (
+ ( '101112131415161718191a1b1c1d1e1f2021222324252627',
+ '112233445566778899aabbccddee' ),
+ 'fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0',
+ '85632d07c6e8f37f950acd320a2ecc93',
+ AES
+ ),
+
+ # RFC5297, A.2
+ (
+ ( '00112233445566778899aabbccddeeffdeaddadadeaddadaffeeddcc'+
+ 'bbaa99887766554433221100',
+ '102030405060708090a0',
+ '09f911029d74e35bd84156c5635688c0',
+ '7468697320697320736f6d6520706c61'+
+ '696e7465787420746f20656e63727970'+
+ '74207573696e67205349562d414553'),
+ '7f7e7d7c7b7a79787776757473727170',
+ '7bdb6e3b432667eb06f4d14bff2fbd0f',
+ AES
+ ),
+
+ ]
+
+ def test1(self):
+ """Verify correctness of test vector"""
+ for tv in self._testData:
+ s2v = _S2V.new(t2b(tv[1]), tv[3])
+ for s in tv[0]:
+ s2v.update(t2b(s))
+ result = s2v.derive()
+ self.assertEqual(result, t2b(tv[2]))
+
+ def test2(self):
+ """Verify that no more than 127(AES) and 63(TDES)
+ components are accepted."""
+ key = bchr(0) * 8 + bchr(255) * 8
+ for module in (AES, DES3):
+ s2v = _S2V.new(key, module)
+ max_comps = module.block_size*8-1
+ for i in range(max_comps):
+ s2v.update(b("XX"))
+ self.assertRaises(TypeError, s2v.update, b("YY"))
+
+
+class HKDF_Tests(unittest.TestCase):
+
+ # Test vectors from RFC5869, Appendix A
+ # Each tuple is made up by:
+ # Item #0: hash module
+ # Item #1: secret
+ # Item #2: salt
+ # Item #3: context
+ # Item #4: expected result
+ _test_vector = (
+ (
+ SHA256,
+ "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+ "000102030405060708090a0b0c",
+ "f0f1f2f3f4f5f6f7f8f9",
+ 42,
+ "3cb25f25faacd57a90434f64d0362f2a" +
+ "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" +
+ "34007208d5b887185865"
+ ),
+ (
+ SHA256,
+ "000102030405060708090a0b0c0d0e0f" +
+ "101112131415161718191a1b1c1d1e1f" +
+ "202122232425262728292a2b2c2d2e2f" +
+ "303132333435363738393a3b3c3d3e3f" +
+ "404142434445464748494a4b4c4d4e4f",
+ "606162636465666768696a6b6c6d6e6f" +
+ "707172737475767778797a7b7c7d7e7f" +
+ "808182838485868788898a8b8c8d8e8f" +
+ "909192939495969798999a9b9c9d9e9f" +
+ "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
+ "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" +
+ "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
+ "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" +
+ "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
+ "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
+ 82,
+ "b11e398dc80327a1c8e7f78c596a4934" +
+ "4f012eda2d4efad8a050cc4c19afa97c" +
+ "59045a99cac7827271cb41c65e590e09" +
+ "da3275600c2f09b8367793a9aca3db71" +
+ "cc30c58179ec3e87c14c01d5c1f3434f" +
+ "1d87"
+ ),
+ (
+ SHA256,
+ "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+ None,
+ None,
+ 42,
+ "8da4e775a563c18f715f802a063c5a31" +
+ "b8a11f5c5ee1879ec3454e5f3c738d2d" +
+ "9d201395faa4b61a96c8"
+ ),
+ (
+ SHA1,
+ "0b0b0b0b0b0b0b0b0b0b0b",
+ "000102030405060708090a0b0c",
+ "f0f1f2f3f4f5f6f7f8f9",
+ 42,
+ "085a01ea1b10f36933068b56efa5ad81" +
+ "a4f14b822f5b091568a9cdd4f155fda2" +
+ "c22e422478d305f3f896"
+ ),
+ (
+ SHA1,
+ "000102030405060708090a0b0c0d0e0f" +
+ "101112131415161718191a1b1c1d1e1f" +
+ "202122232425262728292a2b2c2d2e2f" +
+ "303132333435363738393a3b3c3d3e3f" +
+ "404142434445464748494a4b4c4d4e4f",
+ "606162636465666768696a6b6c6d6e6f" +
+ "707172737475767778797a7b7c7d7e7f" +
+ "808182838485868788898a8b8c8d8e8f" +
+ "909192939495969798999a9b9c9d9e9f" +
+ "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
+ "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" +
+ "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" +
+ "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" +
+ "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" +
+ "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
+ 82,
+ "0bd770a74d1160f7c9f12cd5912a06eb" +
+ "ff6adcae899d92191fe4305673ba2ffe" +
+ "8fa3f1a4e5ad79f3f334b3b202b2173c" +
+ "486ea37ce3d397ed034c7f9dfeb15c5e" +
+ "927336d0441f4c4300e2cff0d0900b52" +
+ "d3b4"
+ ),
+ (
+ SHA1,
+ "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+ "",
+ "",
+ 42,
+ "0ac1af7002b3d761d1e55298da9d0506" +
+ "b9ae52057220a306e07b6b87e8df21d0" +
+ "ea00033de03984d34918"
+ ),
+ (
+ SHA1,
+ "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
+ None,
+ "",
+ 42,
+ "2c91117204d745f3500d636a62f64f0a" +
+ "b3bae548aa53d423b0d1f27ebba6f5e5" +
+ "673a081d70cce7acfc48"
+ )
+ )
+
+ def test1(self):
+ for tv in self._test_vector:
+ secret, salt, info, exp = [ t2b(tv[x]) for x in (1,2,3,5) ]
+ key_len, hashmod = [ tv[x] for x in (4,0) ]
+
+ output = HKDF(secret, key_len, salt, hashmod, 1, info)
+ self.assertEqual(output, exp)
+
+ def test2(self):
+ ref = HKDF(b("XXXXXX"), 12, b("YYYY"), SHA1)
+
+ # Same output, but this time split over 2 keys
+ key1, key2 = HKDF(b("XXXXXX"), 6, b("YYYY"), SHA1, 2)
+ self.assertEqual((ref[:6], ref[6:]), (key1, key2))
+
+ # Same output, but this time split over 3 keys
+ key1, key2, key3 = HKDF(b("XXXXXX"), 4, b("YYYY"), SHA1, 3)
+ self.assertEqual((ref[:4], ref[4:8], ref[8:]), (key1, key2, key3))
+
+
+class scrypt_Tests(unittest.TestCase):
+
+ # Test vectors taken from
+ # https://tools.ietf.org/html/rfc7914
+ # - password
+ # - salt
+ # - N
+ # - r
+ # - p
+ data = (
+ (
+ "",
+ "",
+ 16, # 2K
+ 1,
+ 1,
+ """
+ 77 d6 57 62 38 65 7b 20 3b 19 ca 42 c1 8a 04 97
+ f1 6b 48 44 e3 07 4a e8 df df fa 3f ed e2 14 42
+ fc d0 06 9d ed 09 48 f8 32 6a 75 3a 0f c8 1f 17
+ e8 d3 e0 fb 2e 0d 36 28 cf 35 e2 0c 38 d1 89 06
+ """
+ ),
+ (
+ "password",
+ "NaCl",
+ 1024, # 1M
+ 8,
+ 16,
+ """
+ fd ba be 1c 9d 34 72 00 78 56 e7 19 0d 01 e9 fe
+ 7c 6a d7 cb c8 23 78 30 e7 73 76 63 4b 37 31 62
+ 2e af 30 d9 2e 22 a3 88 6f f1 09 27 9d 98 30 da
+ c7 27 af b9 4a 83 ee 6d 83 60 cb df a2 cc 06 40
+ """
+ ),
+ (
+ "pleaseletmein",
+ "SodiumChloride",
+ 16384, # 16M
+ 8,
+ 1,
+ """
+ 70 23 bd cb 3a fd 73 48 46 1c 06 cd 81 fd 38 eb
+ fd a8 fb ba 90 4f 8e 3e a9 b5 43 f6 54 5d a1 f2
+ d5 43 29 55 61 3f 0f cf 62 d4 97 05 24 2a 9a f9
+ e6 1e 85 dc 0d 65 1e 40 df cf 01 7b 45 57 58 87
+ """
+ ),
+ (
+ "pleaseletmein",
+ "SodiumChloride",
+ 1048576, # 1G
+ 8,
+ 1,
+ """
+ 21 01 cb 9b 6a 51 1a ae ad db be 09 cf 70 f8 81
+ ec 56 8d 57 4a 2f fd 4d ab e5 ee 98 20 ad aa 47
+ 8e 56 fd 8f 4b a5 d0 9f fa 1c 6d 92 7c 40 f4 c3
+ 37 30 40 49 e8 a9 52 fb cb f4 5c 6f a7 7a 41 a4
+ """
+ ),
+ )
+
+ def setUp(self):
+ new_test_vectors = []
+ for tv in self.data:
+ new_tv = TestVector()
+ new_tv.P = b(tv[0])
+ new_tv.S = b(tv[1])
+ new_tv.N = tv[2]
+ new_tv.r = tv[3]
+ new_tv.p = tv[4]
+ new_tv.output = t2b(tv[5])
+ new_tv.dkLen = len(new_tv.output)
+ new_test_vectors.append(new_tv)
+ self.data = new_test_vectors
+
+ def test2(self):
+
+ for tv in self.data:
+ try:
+ output = scrypt(tv.P, tv.S, tv.dkLen, tv.N, tv.r, tv.p)
+ except ValueError as e:
+ if " 2 " in str(e) and tv.N >= 1048576:
+ import warnings
+ warnings.warn("Not enough memory to unit test scrypt() with N=1048576", RuntimeWarning)
+ continue
+ else:
+ raise e
+ self.assertEqual(output, tv.output)
+
+ def test3(self):
+ ref = scrypt(b("password"), b("salt"), 12, 16, 1, 1)
+
+ # Same output, but this time split over 2 keys
+ key1, key2 = scrypt(b("password"), b("salt"), 6, 16, 1, 1, 2)
+ self.assertEqual((ref[:6], ref[6:]), (key1, key2))
+
+ # Same output, but this time split over 3 keys
+ key1, key2, key3 = scrypt(b("password"), b("salt"), 4, 16, 1, 1, 3)
+ self.assertEqual((ref[:4], ref[4:8], ref[8:]), (key1, key2, key3))
+
+
+class bcrypt_Tests(unittest.TestCase):
+
+ def test_negative_cases(self):
+ self.assertRaises(ValueError, bcrypt, b"1" * 73, 10)
+ self.assertRaises(ValueError, bcrypt, b"1" * 10, 3)
+ self.assertRaises(ValueError, bcrypt, b"1" * 10, 32)
+ self.assertRaises(ValueError, bcrypt, b"1" * 10, 4, salt=b"")
+ self.assertRaises(ValueError, bcrypt, b"1" * 10, 4, salt=b"1")
+ self.assertRaises(ValueError, bcrypt, b"1" * 10, 4, salt=b"1" * 17)
+ self.assertRaises(ValueError, bcrypt, b"1\x00" * 10, 4)
+
+ def test_bytearray_mismatch(self):
+ ref = bcrypt("pwd", 4)
+ bcrypt_check("pwd", ref)
+ bref = bytearray(ref)
+ bcrypt_check("pwd", bref)
+
+ wrong = ref[:-1] + bchr(bref[-1] ^ 0x01)
+ self.assertRaises(ValueError, bcrypt_check, "pwd", wrong)
+
+ wrong = b"x" + ref[1:]
+ self.assertRaises(ValueError, bcrypt_check, "pwd", wrong)
+
+ # https://github.com/patrickfav/bcrypt/wiki/Published-Test-Vectors
+
+ def test_empty_password(self):
+ # password, cost, salt, bcrypt hash
+ tvs = [
+ (b"", 4, b"zVHmKQtGGQob.b/Nc7l9NO", b"$2a$04$zVHmKQtGGQob.b/Nc7l9NO8UlrYcW05FiuCj/SxsFO/ZtiN9.mNzy"),
+ (b"", 5, b"zVHmKQtGGQob.b/Nc7l9NO", b"$2a$05$zVHmKQtGGQob.b/Nc7l9NOWES.1hkVBgy5IWImh9DOjKNU8atY4Iy"),
+ (b"", 6, b"zVHmKQtGGQob.b/Nc7l9NO", b"$2a$06$zVHmKQtGGQob.b/Nc7l9NOjOl7l4oz3WSh5fJ6414Uw8IXRAUoiaO"),
+ (b"", 7, b"zVHmKQtGGQob.b/Nc7l9NO", b"$2a$07$zVHmKQtGGQob.b/Nc7l9NOBsj1dQpBA1HYNGpIETIByoNX9jc.hOi"),
+ (b"", 8, b"zVHmKQtGGQob.b/Nc7l9NO", b"$2a$08$zVHmKQtGGQob.b/Nc7l9NOiLTUh/9MDpX86/DLyEzyiFjqjBFePgO"),
+ ]
+
+ for (idx, (password, cost, salt64, result)) in enumerate(tvs):
+ x = bcrypt(password, cost, salt=_bcrypt_decode(salt64))
+ self.assertEqual(x, result)
+ bcrypt_check(password, result)
+
+ def test_random_password_and_salt_short_pw(self):
+ # password, cost, salt, bcrypt hash
+ tvs = [
+ (b"<.S.2K(Zq'", 4, b"VYAclAMpaXY/oqAo9yUpku", b"$2a$04$VYAclAMpaXY/oqAo9yUpkuWmoYywaPzyhu56HxXpVltnBIfmO9tgu"),
+ (b"5.rApO%5jA", 5, b"kVNDrnYKvbNr5AIcxNzeIu", b"$2a$05$kVNDrnYKvbNr5AIcxNzeIuRcyIF5cZk6UrwHGxENbxP5dVv.WQM/G"),
+ (b"oW++kSrQW^", 6, b"QLKkRMH9Am6irtPeSKN5sO", b"$2a$06$QLKkRMH9Am6irtPeSKN5sObJGr3j47cO6Pdf5JZ0AsJXuze0IbsNm"),
+ (b"ggJ\\KbTnDG", 7, b"4H896R09bzjhapgCPS/LYu", b"$2a$07$4H896R09bzjhapgCPS/LYuMzAQluVgR5iu/ALF8L8Aln6lzzYXwbq"),
+ (b"49b0:;VkH/", 8, b"hfvO2retKrSrx5f2RXikWe", b"$2a$08$hfvO2retKrSrx5f2RXikWeFWdtSesPlbj08t/uXxCeZoHRWDz/xFe"),
+ (b">9N^5jc##'", 9, b"XZLvl7rMB3EvM0c1.JHivu", b"$2a$09$XZLvl7rMB3EvM0c1.JHivuIDPJWeNJPTVrpjZIEVRYYB/mF6cYgJK"),
+ (b"\\$ch)s4WXp", 10, b"aIjpMOLK5qiS9zjhcHR5TO", b"$2a$10$aIjpMOLK5qiS9zjhcHR5TOU7v2NFDmcsBmSFDt5EHOgp/jeTF3O/q"),
+ (b"RYoj\\_>2P7", 12, b"esIAHiQAJNNBrsr5V13l7.", b"$2a$12$esIAHiQAJNNBrsr5V13l7.RFWWJI2BZFtQlkFyiWXjou05GyuREZa"),
+ ]
+
+ for (idx, (password, cost, salt64, result)) in enumerate(tvs):
+ x = bcrypt(password, cost, salt=_bcrypt_decode(salt64))
+ self.assertEqual(x, result)
+ bcrypt_check(password, result)
+
+ def test_random_password_and_salt_long_pw(self):
+ # password, cost, salt, bcrypt hash
+ tvs = [
+ (b"^Q&\"]A`%/A(BVGt>QaX0M-#1ghq_+\":Y0CRmY", 5, b"YuQvhokOGVnevctykUYpKu", b"$2a$05$YuQvhokOGVnevctykUYpKutZD2pWeGGYn3auyLOasguMY3/0BbIyq"),
+ (b"F%uN/j>[GuB7-jB'_Yj!Tnb7Y!u^6)", 6, b"5L3vpQ0tG9O7k5gQ8nAHAe", b"$2a$06$5L3vpQ0tG9O7k5gQ8nAHAe9xxQiOcOLh8LGcI0PLWhIznsDt.S.C6"),
+ (b"Z>BobP32ub\"Cfe*Q<-q-=tRSjOBh8\\mLNW.", 9, b"nArqOfdCsD9kIbVnAixnwe", b"$2a$09$nArqOfdCsD9kIbVnAixnwe6s8QvyPYWtQBpEXKir2OJF9/oNBsEFe"),
+ (b"/MH51`!BP&0tj3%YCA;Xk%e3S`o\\EI", 10, b"ePiAc.s.yoBi3B6p1iQUCe", b"$2a$10$ePiAc.s.yoBi3B6p1iQUCezn3mraLwpVJ5XGelVyYFKyp5FZn/y.u"),
+ (b"ptAP\"mcg6oH.\";c0U2_oll.OKi5?Ui\"^ai#iQH7ZFtNMfs3AROnIncE9\"BNNoEgO[[*Yk8;RQ(#S,;I+aT",
+ 5, b"wgkOlGNXIVE2fWkT3gyRoO", b"$2a$05$wgkOlGNXIVE2fWkT3gyRoOqWi4gbi1Wv2Q2Jx3xVs3apl1w.Wtj8C"),
+ (b"M.E1=dt<.L0Q&p;94NfGm_Oo23+Kpl@M5?WIAL.[@/:'S)W96G8N^AWb7_smmC]>7#fGoB",
+ 6, b"W9zTCl35nEvUukhhFzkKMe", b"$2a$06$W9zTCl35nEvUukhhFzkKMekjT9/pj7M0lihRVEZrX3m8/SBNZRX7i"),
+ ]
+
+ for (idx, (password, cost, salt64, result)) in enumerate(tvs):
+ x = bcrypt(password, cost, salt=_bcrypt_decode(salt64))
+ self.assertEqual(x, result)
+ bcrypt_check(password, result)
+
+ def test_increasing_password_length(self):
+ # password, cost, salt, bcrypt hash
+ tvs = [
+ (b"a", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.l4WvgHIVg17ZawDIrDM2IjlE64GDNQS"),
+ (b"aa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.AyUxBk.ThHlsLvRTH7IqcG7yVHJ3SXq"),
+ (b"aaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.BxOVac5xPB6XFdRc/ZrzM9FgZkqmvbW"),
+ (b"aaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.Qbr209bpCtfl5hN7UQlG/L4xiD3AKau"),
+ (b"aaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.oWszihPjDZI0ypReKsaDOW1jBl7oOii"),
+ (b"aaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ./k.Xxn9YiqtV/sxh3EHbnOHd0Qsq27K"),
+ (b"aaaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.PYJqRFQbgRbIjMd5VNKmdKS4sBVOyDe"),
+ (b"aaaaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ..VMYfzaw1wP/SGxowpLeGf13fxCCt.q"),
+ (b"aaaaaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.5B0p054nO5WgAD1n04XslDY/bqY9RJi"),
+ (b"aaaaaaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.INBTgqm7sdlBJDg.J5mLMSRK25ri04y"),
+ (b"aaaaaaaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.s3y7CdFD0OR5p6rsZw/eZ.Dla40KLfm"),
+ (b"aaaaaaaaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.Jx742Djra6Q7PqJWnTAS.85c28g.Siq"),
+ (b"aaaaaaaaaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.oKMXW3EZcPHcUV0ib5vDBnh9HojXnLu"),
+ (b"aaaaaaaaaaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.w6nIjWpDPNSH5pZUvLjC1q25ONEQpeS"),
+ (b"aaaaaaaaaaaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.k1b2/r9A/hxdwKEKurg6OCn4MwMdiGq"),
+ (b"aaaaaaaaaaaaaaaa", 4, b"5DCebwootqWMCp59ISrMJ.", b"$2a$04$5DCebwootqWMCp59ISrMJ.3prCNHVX1Ws.7Hm2bJxFUnQOX9f7DFa"),
+ ]
+
+ for (idx, (password, cost, salt64, result)) in enumerate(tvs):
+ x = bcrypt(password, cost, salt=_bcrypt_decode(salt64))
+ self.assertEqual(x, result)
+ bcrypt_check(password, result)
+
+ def test_non_ascii_characters(self):
+ # password, cost, salt, bcrypt hash
+ tvs = [
+ ("àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝðÐ", 4, b"D3qS2aoTVyqM7z8v8crLm.", b"$2a$04$D3qS2aoTVyqM7z8v8crLm.3nKt4CzBZJbyFB.ZebmfCvRw7BGs.Xm"),
+ ("àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝðÐ", 5, b"VA1FujiOCMPkUHQ8kF7IaO", b"$2a$05$VA1FujiOCMPkUHQ8kF7IaOg7NGaNvpxwWzSluQutxEVmbZItRTsAa"),
+ ("àèìòùÀÈÌÒÙáéíóúýÁÉÍÓÚÝðÐ", 6, b"TXiaNrPeBSz5ugiQlehRt.", b"$2a$06$TXiaNrPeBSz5ugiQlehRt.gwpeDQnXWteQL4z2FulouBr6G7D9KUi"),
+ ("âêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿ", 4, b"YTn1Qlvps8e1odqMn6G5x.", b"$2a$04$YTn1Qlvps8e1odqMn6G5x.85pqKql6w773EZJAExk7/BatYAI4tyO"),
+ ("âêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿ", 5, b"C.8k5vJKD2NtfrRI9o17DO", b"$2a$05$C.8k5vJKD2NtfrRI9o17DOfIW0XnwItA529vJnh2jzYTb1QdoY0py"),
+ ("âêîôûÂÊÎÔÛãñõÃÑÕäëïöüÿ", 6, b"xqfRPj3RYAgwurrhcA6uRO", b"$2a$06$xqfRPj3RYAgwurrhcA6uROtGlXDp/U6/gkoDYHwlubtcVcNft5.vW"),
+ ("ÄËÏÖÜŸåÅæÆœŒßçÇøØ¢¿¡€", 4, b"y8vGgMmr9EdyxP9rmMKjH.", b"$2a$04$y8vGgMmr9EdyxP9rmMKjH.wv2y3r7yRD79gykQtmb3N3zrwjKsyay"),
+ ("ÄËÏÖÜŸåÅæÆœŒßçÇøØ¢¿¡€", 5, b"iYH4XIKAOOm/xPQs7xKP1u", b"$2a$05$iYH4XIKAOOm/xPQs7xKP1upD0cWyMn3Jf0ZWiizXbEkVpS41K1dcO"),
+ ("ÄËÏÖÜŸåÅæÆœŒßçÇøØ¢¿¡€", 6, b"wCOob.D0VV8twafNDB2ape", b"$2a$06$wCOob.D0VV8twafNDB2apegiGD5nqF6Y1e6K95q6Y.R8C4QGd265q"),
+ ("ΔημοσιεύθηκεστηνΕφημερίδατης", 4, b"E5SQtS6P4568MDXW7cyUp.", b"$2a$04$E5SQtS6P4568MDXW7cyUp.18wfDisKZBxifnPZjAI1d/KTYMfHPYO"),
+ ("АБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмН", 4, b"03e26gQFHhQwRNf81/ww9.", b"$2a$04$03e26gQFHhQwRNf81/ww9.p1UbrNwxpzWjLuT.zpTLH4t/w5WhAhC"),
+ ("нОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮю", 4, b"PHNoJwpXCfe32nUtLv2Upu", b"$2a$04$PHNoJwpXCfe32nUtLv2UpuhJXOzd4k7IdFwnEpYwfJVCZ/f/.8Pje"),
+ ("電电電島岛島兔兔兎龜龟亀國国国區区区", 4, b"wU4/0i1TmNl2u.1jIwBX.u", b"$2a$04$wU4/0i1TmNl2u.1jIwBX.uZUaOL3Rc5ID7nlQRloQh6q5wwhV/zLW"),
+ ("诶比伊艾弗豆贝尔维吾艾尺开艾丝维贼德", 4, b"P4kreGLhCd26d4WIy7DJXu", b"$2a$04$P4kreGLhCd26d4WIy7DJXusPkhxLvBouzV6OXkL5EB0jux0osjsry"),
+ ]
+
+ for (idx, (password, cost, salt64, result)) in enumerate(tvs):
+ x = bcrypt(password, cost, salt=_bcrypt_decode(salt64))
+ self.assertEqual(x, result)
+ bcrypt_check(password, result)
+
+ def test_special_case_salt(self):
+ # password, cost, salt, bcrypt hash
+ tvs = [
+ ("-O_=*N!2JP", 4, b"......................", b"$2a$04$......................JjuKLOX9OOwo5PceZZXSkaLDvdmgb82"),
+ ("7B[$Q<4b>U", 5, b"......................", b"$2a$05$......................DRiedDQZRL3xq5A5FL8y7/6NM8a2Y5W"),
+ (">d5-I_8^.h", 6, b"......................", b"$2a$06$......................5Mq1Ng8jgDY.uHNU4h5p/x6BedzNH2W"),
+ (")V`/UM/]1t", 4, b".OC/.OC/.OC/.OC/.OC/.O", b"$2a$04$.OC/.OC/.OC/.OC/.OC/.OQIvKRDAam.Hm5/IaV/.hc7P8gwwIbmi"),
+ (":@t2.bWuH]", 5, b".OC/.OC/.OC/.OC/.OC/.O", b"$2a$05$.OC/.OC/.OC/.OC/.OC/.ONDbUvdOchUiKmQORX6BlkPofa/QxW9e"),
+ ("b(#KljF5s\"", 6, b".OC/.OC/.OC/.OC/.OC/.O", b"$2a$06$.OC/.OC/.OC/.OC/.OC/.OHfTd9e7svOu34vi1PCvOcAEq07ST7.K"),
+ ("@3YaJ^Xs]*", 4, b"eGA.eGA.eGA.eGA.eGA.e.", b"$2a$04$eGA.eGA.eGA.eGA.eGA.e.stcmvh.R70m.0jbfSFVxlONdj1iws0C"),
+ ("'\"5\\!k*C(p", 5, b"eGA.eGA.eGA.eGA.eGA.e.", b"$2a$05$eGA.eGA.eGA.eGA.eGA.e.vR37mVSbfdHwu.F0sNMvgn8oruQRghy"),
+ ("edEu7C?$'W", 6, b"eGA.eGA.eGA.eGA.eGA.e.", b"$2a$06$eGA.eGA.eGA.eGA.eGA.e.tSq0FN8MWHQXJXNFnHTPQKtA.n2a..G"),
+ ("N7dHmg\\PI^", 4, b"999999999999999999999u", b"$2a$04$999999999999999999999uCZfA/pLrlyngNDMq89r1uUk.bQ9icOu"),
+ ("\"eJuHh!)7*", 5, b"999999999999999999999u", b"$2a$05$999999999999999999999uj8Pfx.ufrJFAoWFLjapYBS5vVEQQ/hK"),
+ ("ZeDRJ:_tu:", 6, b"999999999999999999999u", b"$2a$06$999999999999999999999u6RB0P9UmbdbQgjoQFEJsrvrKe.BoU6q"),
+ ]
+
+ for (idx, (password, cost, salt64, result)) in enumerate(tvs):
+ x = bcrypt(password, cost, salt=_bcrypt_decode(salt64))
+ self.assertEqual(x, result)
+ bcrypt_check(password, result)
+
+
+class TestVectorsHKDFWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._id = "None"
+
+ def add_tests(self, filename):
+
+ def filter_algo(root):
+ algo_name = root['algorithm']
+ if algo_name == "HKDF-SHA-1":
+ return SHA1
+ elif algo_name == "HKDF-SHA-256":
+ return SHA256
+ elif algo_name == "HKDF-SHA-384":
+ return SHA384
+ elif algo_name == "HKDF-SHA-512":
+ return SHA512
+ else:
+ raise ValueError("Unknown algorithm " + algo_name)
+
+ def filter_size(unit):
+ return int(unit['size'])
+
+ result = load_test_vectors_wycheproof(("Protocol", "wycheproof"),
+ filename,
+ "Wycheproof HMAC (%s)" % filename,
+ root_tag={'hash_module': filter_algo},
+ unit_tag={'size': filter_size})
+ return result
+
+ def setUp(self):
+ self.tv = []
+ self.add_tests("hkdf_sha1_test.json")
+ self.add_tests("hkdf_sha256_test.json")
+ self.add_tests("hkdf_sha384_test.json")
+ self.add_tests("hkdf_sha512_test.json")
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_verify(self, tv):
+ self._id = "Wycheproof HKDF Test #%d (%s, %s)" % (tv.id, tv.comment, tv.filename)
+
+ try:
+ key = HKDF(tv.ikm, tv.size, tv.salt, tv.hash_module, 1, tv.info)
+ except ValueError:
+ assert not tv.valid
+ else:
+ if key != tv.okm:
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.warn(tv)
+
+ def runTest(self):
+ for tv in self.tv:
+ self.test_verify(tv)
+
+
+def load_hash_by_name(hash_name):
+ return __import__("Crypto.Hash." + hash_name, globals(), locals(), ["new"])
+
+
+class SP800_108_Counter_Tests(unittest.TestCase):
+
+ def test_negative_zeroes(self):
+ def prf(s, x):
+ return HMAC.new(s, x, SHA256).digest()
+
+ try:
+ _ = SP800_108_Counter(b'0' * 16, 1, prf, label=b'A\x00B')
+ except ValueError:
+ self.fail('SP800_108_Counter failed with zero in label')
+ self.assertRaises(ValueError, SP800_108_Counter, b'0' * 16, 1, prf,
+ context=b'A\x00B')
+
+ def test_multiple_keys(self):
+ def prf(s, x):
+ return HMAC.new(s, x, SHA256).digest()
+
+ key = b'0' * 16
+ expected = SP800_108_Counter(key, 2*3*23, prf)
+ for r in (1, 2, 3, 23):
+ dks = SP800_108_Counter(key, r, prf, 138//r)
+ self.assertEqual(len(dks), 138//r)
+ self.assertEqual(len(dks[0]), r)
+ self.assertEqual(b''.join(dks), expected)
+
+
+def add_tests_sp800_108_counter(cls):
+
+ test_vectors_sp800_108_counter = load_test_vectors(("Protocol", ),
+ "KDF_SP800_108_COUNTER.txt",
+ "NIST SP 800 108 KDF Counter Mode",
+ {'count': lambda x: int(x)},
+ ) or []
+
+ mac_type = None
+ for idx, tv in enumerate(test_vectors_sp800_108_counter):
+
+ if isinstance(tv, str):
+ res = re.match(r"\[HMAC-(SHA-[0-9]+)\]", tv)
+ if res:
+ hash_name = res.group(1).replace("-", "")
+ hash_module = load_hash_by_name(hash_name)
+ mac_type = "hmac"
+ continue
+ res = re.match(r"\[CMAC-AES-128\]", tv)
+ if res:
+ mac_type = "cmac"
+ continue
+ assert res
+
+ if mac_type == "hmac":
+ def prf(s, x, hash_module=hash_module):
+ return HMAC.new(s, x, hash_module).digest()
+ elif mac_type == "cmac":
+ def prf(s, x, hash_module=hash_module):
+ return CMAC.new(s, x, AES).digest()
+ continue
+
+ def kdf_test(self, prf=prf, kin=tv.kin, label=tv.label,
+ context=tv.context, kout=tv.kout, count=tv.count):
+ result = SP800_108_Counter(kin, len(kout), prf, 1, label, context)
+ assert(len(result) == len(kout))
+ self.assertEqual(result, kout)
+
+ setattr(cls, "test_kdf_sp800_108_counter_%d" % idx, kdf_test)
+
+
+add_tests_sp800_108_counter(SP800_108_Counter_Tests)
+
+
+def get_tests(config={}):
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ if not config.get('slow_tests'):
+ PBKDF2_Tests._testData = PBKDF2_Tests._testData[:3]
+ scrypt_Tests.data = scrypt_Tests.data[:3]
+
+ tests = []
+ tests += list_test_cases(PBKDF1_Tests)
+ tests += list_test_cases(PBKDF2_Tests)
+ tests += list_test_cases(S2V_Tests)
+ tests += list_test_cases(HKDF_Tests)
+ tests += [TestVectorsHKDFWycheproof(wycheproof_warnings)]
+ tests += list_test_cases(scrypt_Tests)
+ tests += list_test_cases(bcrypt_Tests)
+ tests += list_test_cases(SP800_108_Counter_Tests)
+
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_SecretSharing.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_SecretSharing.py
new file mode 100644
index 00000000..b514b963
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_SecretSharing.py
@@ -0,0 +1,290 @@
+#
+# SelfTest/Protocol/test_secret_sharing.py: Self-test for secret sharing protocols
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 unittest import main, TestCase, TestSuite
+from binascii import unhexlify, hexlify
+
+from Crypto.Util.py3compat import *
+from Crypto.Hash import SHAKE128
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Protocol.SecretSharing import Shamir, _Element, \
+ _mult_gf2, _div_gf2
+
+
+class GF2_Tests(TestCase):
+
+ def test_mult_gf2(self):
+ # Prove mult by zero
+ x = _mult_gf2(0,0)
+ self.assertEqual(x, 0)
+
+ # Prove mult by unity
+ x = _mult_gf2(34, 1)
+ self.assertEqual(x, 34)
+
+ z = 3 # (x+1)
+ y = _mult_gf2(z, z)
+ self.assertEqual(y, 5) # (x+1)^2 = x^2 + 1
+ y = _mult_gf2(y, z)
+ self.assertEqual(y, 15) # (x+1)^3 = x^3 + x^2 + x + 1
+ y = _mult_gf2(y, z)
+ self.assertEqual(y, 17) # (x+1)^4 = x^4 + 1
+
+ # Prove linearity works
+ comps = [1, 4, 128, 2**34]
+ sum_comps = 1+4+128+2**34
+ y = 908
+ z = _mult_gf2(sum_comps, y)
+ w = 0
+ for x in comps:
+ w ^= _mult_gf2(x, y)
+ self.assertEqual(w, z)
+
+ def test_div_gf2(self):
+ from Crypto.Util.number import size as deg
+
+ x, y = _div_gf2(567, 7)
+ self.assertTrue(deg(y) < deg(7))
+
+ w = _mult_gf2(x, 7) ^ y
+ self.assertEqual(567, w)
+
+ x, y = _div_gf2(7, 567)
+ self.assertEqual(x, 0)
+ self.assertEqual(y, 7)
+
+class Element_Tests(TestCase):
+
+ def test1(self):
+ # Test encondings
+ e = _Element(256)
+ self.assertEqual(int(e), 256)
+ self.assertEqual(e.encode(), bchr(0)*14 + b("\x01\x00"))
+
+ e = _Element(bchr(0)*14 + b("\x01\x10"))
+ self.assertEqual(int(e), 0x110)
+ self.assertEqual(e.encode(), bchr(0)*14 + b("\x01\x10"))
+
+ # Only 16 byte string are a valid encoding
+ self.assertRaises(ValueError, _Element, bchr(0))
+
+ def test2(self):
+ # Test addition
+ e = _Element(0x10)
+ f = _Element(0x0A)
+ self.assertEqual(int(e+f), 0x1A)
+
+ def test3(self):
+ # Test multiplication
+ zero = _Element(0)
+ one = _Element(1)
+ two = _Element(2)
+
+ x = _Element(6) * zero
+ self.assertEqual(int(x), 0)
+
+ x = _Element(6) * one
+ self.assertEqual(int(x), 6)
+
+ x = _Element(2**127) * two
+ self.assertEqual(int(x), 1 + 2 + 4 + 128)
+
+ def test4(self):
+ # Test inversion
+ one = _Element(1)
+
+ x = one.inverse()
+ self.assertEqual(int(x), 1)
+
+ x = _Element(82323923)
+ y = x.inverse()
+ self.assertEqual(int(x * y), 1)
+
+
+class Shamir_Tests(TestCase):
+
+ def test1(self):
+ # Test splitting
+ shares = Shamir.split(2, 3, bchr(90)*16)
+ self.assertEqual(len(shares), 3)
+ for index in range(3):
+ self.assertEqual(shares[index][0], index+1)
+ self.assertEqual(len(shares[index][1]), 16)
+
+ def test2(self):
+ # Test recombine
+ from itertools import permutations
+
+ # Generated by ssss (index, secret, shares)
+ # in hex mode, without "diffusion" mode
+ test_vectors = (
+ (2, "d9fe73909bae28b3757854c0af7ad405",
+ "1-594ae8964294174d95c33756d2504170",
+ "2-d897459d29da574eb40e93ec552ffe6e",
+ "3-5823de9bf0e068b054b5f07a28056b1b",
+ "4-db2c1f8bff46d748f795da995bd080cb"),
+ (2, "bf4f902d9a7efafd1f3ffd9291fd5de9",
+ "1-557bd3b0748064b533469722d1cc7935",
+ "2-6b2717164783c66d47cd28f2119f14d0",
+ "3-8113548ba97d58256bb4424251ae300c",
+ "4-179e9e5a218483ddaeda57539139cf04"),
+ (3, "ec96aa5c14c9faa699354cf1da74e904",
+ "1-64579fbf1908d66f7239bf6e2b4e41e1",
+ "2-6cd9428df8017b52322561e8c672ae3e",
+ "3-e418776ef5c0579bd9299277374806dd",
+ "4-ab3f77a0107398d23b323e581bb43f5d",
+ "5-23fe42431db2b41bd03ecdc7ea8e97ac"),
+ (3, "44cf249b68b80fcdc27b47be60c2c145",
+ "1-d6515a3905cd755119b86e311c801e31",
+ "2-16693d9ac9f10c254036ced5f8917fa3",
+ "3-84f74338a48476b99bf5e75a84d3a0d1",
+ "4-3fe8878dc4a5d35811cf3cbcd33dbe52",
+ "5-ad76f92fa9d0a9c4ca0c1533af7f6132"),
+ (5, "5398717c982db935d968eebe53a47f5a",
+ "1-be7be2dd4c068e7ef576aaa1b1c11b01",
+ "2-f821f5848441cb98b3eb467e2733ee21",
+ "3-25ee52f53e203f6e29a0297b5ab486b5",
+ "4-fc9fb58ef74dab947fbf9acd9d5d83cd",
+ "5-b1949cce46d81552e65f248d3f74cc5c",
+ "6-d64797f59977c4d4a7956ad916da7699",
+ "7-ab608a6546a8b9af8820ff832b1135c7"),
+ (5, "4a78db90fbf35da5545d2fb728e87596",
+ "1-08daf9a25d8aa184cfbf02b30a0ed6a0",
+ "2-dda28261e36f0b14168c2cf153fb734e",
+ "3-e9fdec5505d674a57f9836c417c1ecaa",
+ "4-4dce5636ae06dee42d2c82e65f06c735",
+ "5-3963dc118afc2ba798fa1d452b28ef00",
+ "6-6dfe6ff5b09e94d2f84c382b12f42424",
+ "7-6faea9d4d4a4e201bf6c90b9000630c3"),
+ (10, "eccbf6d66d680b49b073c4f1ddf804aa",
+ "01-7d8ac32fe4ae209ead1f3220fda34466",
+ "02-f9144e76988aad647d2e61353a6e96d5",
+ "03-b14c3b80179203363922d60760271c98",
+ "04-770bb2a8c28f6cee89e00f4d5cc7f861",
+ "05-6e3d7073ea368334ef67467871c66799",
+ "06-248792bc74a98ce024477c13c8fb5f8d",
+ "07-fcea4640d2db820c0604851e293d2487",
+ "08-2776c36fb714bb1f8525a0be36fc7dba",
+ "09-6ee7ac8be773e473a4bf75ee5f065762",
+ "10-33657fc073354cf91d4a68c735aacfc8",
+ "11-7645c65094a5868bf225c516fdee2d0c",
+ "12-840485aacb8226631ecd9c70e3018086"),
+ (10, "377e63bdbb5f7d4dc58a483d035212bb",
+ "01-32c53260103be431c843b1a633afe3bd",
+ "02-0107eb16cb8695084d452d2cc50bc7d6",
+ "03-df1e5c66cd755287fb0446faccd72a06",
+ "04-361bbcd5d40797f49dfa1898652da197",
+ "05-160d3ad1512f7dec7fd9344aed318591",
+ "06-659af6d95df4f25beca4fb9bfee3b7e8",
+ "07-37f3b208977bad50b3724566b72bfa9d",
+ "08-6c1de2dfc69c2986142c26a8248eb316",
+ "09-5e19220837a396bd4bc8cd685ff314c3",
+ "10-86e7b864fb0f3d628e46d50c1ba92f1c",
+ "11-065d0082c80b1aea18f4abe0c49df72e",
+ "12-84a09430c1d20ea9f388f3123c3733a3"),
+ )
+
+ def get_share(p):
+ pos = p.find('-')
+ return int(p[:pos]), unhexlify(p[pos + 1:])
+
+ for tv in test_vectors:
+ k = tv[0]
+ secret = unhexlify(tv[1])
+ max_perms = 10
+ for perm, shares_idx in enumerate(permutations(range(2, len(tv)), k)):
+ if perm > max_perms:
+ break
+ shares = [ get_share(tv[x]) for x in shares_idx ]
+ result = Shamir.combine(shares, True)
+ self.assertEqual(secret, result)
+
+ def test3(self):
+ # Loopback split/recombine
+
+ rng = SHAKE128.new(b"test3")
+
+ for _ in range(100):
+
+ secret = rng.read(16)
+
+ shares = Shamir.split(2, 3, secret)
+
+ secret2 = Shamir.combine(shares[:2])
+ self.assertEqual(secret, secret2)
+
+ secret3 = Shamir.combine([ shares[0], shares[2] ])
+ self.assertEqual(secret, secret3)
+
+ def test4(self):
+ # Loopback split/recombine (SSSS)
+
+ rng = SHAKE128.new(b"test4")
+
+ for _ in range(10):
+ secret = rng.read(16)
+
+ shares = Shamir.split(2, 3, secret, ssss=True)
+
+ secret2 = Shamir.combine(shares[:2], ssss=True)
+ self.assertEqual(secret, secret2)
+
+ for _ in range(10):
+ secret = rng.read(16)
+
+ shares = Shamir.split(3, 7, secret, ssss=True)
+
+ secret2 = Shamir.combine([shares[3], shares[4], shares[6]], ssss=True)
+ self.assertEqual(secret, secret2)
+
+
+ def test5(self):
+ # Detect duplicate shares
+ secret = unhexlify(b("000102030405060708090a0b0c0d0e0f"))
+
+ shares = Shamir.split(2, 3, secret)
+ self.assertRaises(ValueError, Shamir.combine, (shares[0], shares[0]))
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(GF2_Tests)
+ tests += list_test_cases(Element_Tests)
+ tests += list_test_cases(Shamir_Tests)
+ return tests
+
+if __name__ == '__main__':
+ suite = lambda: TestSuite(get_tests())
+ main(defaultTest='suite')
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_ecdh.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_ecdh.py
new file mode 100644
index 00000000..9a282ac7
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_ecdh.py
@@ -0,0 +1,770 @@
+import re
+import base64
+import unittest
+from binascii import hexlify, unhexlify
+
+from Crypto.Util.py3compat import bord
+
+from Crypto.Hash import SHA256
+from Crypto.PublicKey import ECC
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors, load_test_vectors_wycheproof
+
+from Crypto.Protocol import DH
+from Crypto.Protocol.DH import (key_agreement,
+ import_x25519_public_key,
+ import_x25519_private_key,
+ import_x448_public_key,
+ import_x448_private_key)
+
+
+class FIPS_ECDH_Tests_KAT(unittest.TestCase):
+ pass
+
+
+test_vectors_verify = load_test_vectors(("Protocol", ),
+ "KAS_ECC_CDH_PrimitiveTest.txt",
+ "ECC CDH Primitive (SP800-56A Section 5.7.1.2)",
+ {
+ 'qcavsx': lambda x: int(x, 16),
+ 'qcavsy': lambda x: int(x, 16),
+ 'diut': lambda x: int(x, 16),
+ 'qiutx': lambda x: int(x, 16),
+ 'qiuty': lambda x: int(x, 16),
+ }) or []
+
+for idx, tv in enumerate(test_vectors_verify):
+
+ # Stand-alone header with curve name
+ if isinstance(tv, str):
+ res = re.match(r"\[([A-Za-z0-9-]+)\]", tv)
+ assert res
+ curve_name = res.group(1)
+ continue
+
+ public_key = ECC.construct(curve=curve_name,
+ point_x=tv.qcavsx,
+ point_y=tv.qcavsy)
+
+ private_key = ECC.construct(curve=curve_name,
+ d=tv.diut)
+
+ exp_response = tv.ziut
+
+ def ecdh_test(self,
+ public_key=public_key,
+ private_key=private_key,
+ exp_response=exp_response):
+ z = key_agreement(
+ static_pub=public_key,
+ static_priv=private_key,
+ kdf=lambda x: x)
+ self.assertEqual(z, exp_response)
+
+ def ecdh_test_rev(self,
+ public_key=public_key,
+ private_key=private_key,
+ exp_response=exp_response):
+ z = key_agreement(
+ static_pub=public_key,
+ static_priv=private_key,
+ kdf=lambda x: x)
+ self.assertEqual(z, exp_response)
+
+ setattr(FIPS_ECDH_Tests_KAT, "test_verify_positive_%d" % idx, ecdh_test)
+ if idx == 1:
+ setattr(FIPS_ECDH_Tests_KAT, "test_verify_positive_rev_%d" % idx, ecdh_test_rev)
+
+
+class TestVectorsECDHWycheproof(unittest.TestCase):
+
+ desc = "Wycheproof ECDH tests"
+
+ def add_tests(self, filename):
+
+ def curve(g):
+ return g['curve']
+
+ def private(u):
+ return int(u['private'], 16)
+
+ result = load_test_vectors_wycheproof(("Protocol", "wycheproof"),
+ filename,
+ "Wycheproof ECDH (%s)"
+ % filename,
+ group_tag={'curve': curve},
+ unit_tag={'private': private},
+ )
+ self.tv += result
+
+ def setUp(self):
+ self.tv = []
+ self.desc = None
+
+ self.add_tests("ecdh_secp224r1_ecpoint_test.json")
+ self.add_tests("ecdh_secp256r1_ecpoint_test.json")
+ self.add_tests("ecdh_secp384r1_ecpoint_test.json")
+ self.add_tests("ecdh_secp521r1_ecpoint_test.json")
+
+ self.add_tests("ecdh_secp224r1_test.json")
+ self.add_tests("ecdh_secp256r1_test.json")
+ self.add_tests("ecdh_secp384r1_test.json")
+ self.add_tests("ecdh_secp521r1_test.json")
+
+ def shortDescription(self):
+ return self.desc
+
+ def test_verify(self, tv):
+
+ if len(tv.public) == 0:
+ return
+
+ try:
+ if bord(tv.public[0]) == 4: # SEC1
+ public_key = ECC.import_key(tv.public, curve_name=tv.curve)
+ else:
+ public_key = ECC.import_key(tv.public)
+ except ValueError:
+ assert tv.warning or not tv.valid
+ return
+
+ private_key = ECC.construct(curve=tv.curve, d=tv.private)
+
+ try:
+ z = key_agreement(static_pub=public_key,
+ static_priv=private_key,
+ kdf=lambda x: x)
+ except ValueError:
+ assert not tv.valid
+ except TypeError as e:
+ assert not tv.valid
+ assert "incompatible curve" in str(e)
+ else:
+ self.assertEqual(z, tv.shared)
+ assert tv.valid
+
+ def runTest(self):
+ for tv in self.tv:
+ self.desc = "Wycheproof ECDH Verify Test #%d (%s, %s)" % (tv.id, tv.comment, tv.filename)
+ self.test_verify(tv)
+
+
+class ECDH_Tests(unittest.TestCase):
+
+ static_priv = ECC.import_key('-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg9VHFVKh2a1aVFifH\n+BiyNaRa2kttEg3165Ye/dJxJ7KhRANCAARImIEXro5ZOcyWU2mq/+d79FEZXtTA\nbKkz1aICQXihQdCMzRNbeNtC9LFLzhu1slRKJ2xsDAlw9r6w6vwtkRzr\n-----END PRIVATE KEY-----')
+ static_pub = ECC.import_key('-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgHhmv8zmZ+Nw8fsZd\ns8tlZflyfw2NE1CRS9DWr3Y3O46hRANCAAS3hZVUCbk+uk3w4S/YOraEVGG+WYpk\nNO/vrwzufUUks2GV2OnBQESe0EBk4Jq8gn4ij8Lvs3rZX2yT+XfeATYd\n-----END PRIVATE KEY-----').public_key()
+
+ eph_priv = ECC.import_key('-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgGPdJmFFFKzLPspIr\nE1T2cEjeIf4ajS9CpneP0e2b3AyhRANCAAQBexAA5BYDcXHs2KOksTYUsst4HhPt\nkp0zkgI2virc3OGJFNGPaCCPfFCQJHwLRaEpiq3SoQlgoBwSc8ZPsl3y\n-----END PRIVATE KEY-----')
+
+ eph_pub = ECC.import_key('-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQghaVZXElSEGEojFKF\nOU0JCpxWUWHvWQUR81gwWrOp76ShRANCAATi1Ib2K+YR3AckD8wxypWef7pw5PRw\ntBaB3RDPyE7IjHZC6yu1DbcXoCdtaw+F5DM+4zpl59n5ZaIy/Yl1BdIy\n-----END PRIVATE KEY-----')
+
+ def test_1(self):
+ # C(0, 2s)
+ kdf = lambda x: SHA256.new(x).digest()
+ z = key_agreement(
+ kdf=kdf,
+ static_pub=self.static_pub,
+ static_priv=self.static_priv)
+ self.assertEqual(hexlify(z),
+ b"3960a1101d1193cbaffef4cc7202ebff783c22c6d2e0d5d530ffc66dc197ea9c")
+
+ def test_2(self):
+ # C(2e, 2s)
+ kdf = lambda x: SHA256.new(x).digest()
+ z = key_agreement(
+ kdf=kdf,
+ static_pub=self.static_pub,
+ static_priv=self.static_priv,
+ eph_pub=self.eph_pub,
+ eph_priv=self.eph_priv)
+ self.assertEqual(hexlify(z),
+ b"7447b733d40c8fab2c633b3dc61e4a8c742f3a6af7e16fb0cc486f5bdb5d6ba2")
+
+ def test_3(self):
+ # C(1e, 2s)
+ kdf = lambda x: SHA256.new(x).digest()
+ z = key_agreement(
+ kdf=kdf,
+ static_pub=self.static_pub,
+ static_priv=self.static_priv,
+ eph_priv=self.eph_priv)
+ self.assertEqual(hexlify(z),
+ b"9e977ae45f33bf67f285d064d83e6632bcafe3a7d33fe571233bab4794ace759")
+
+ def test_4(self):
+ # C(1e, 2s)
+ kdf = lambda x: SHA256.new(x).digest()
+ z = key_agreement(
+ kdf=kdf,
+ static_pub=self.static_pub,
+ static_priv=self.static_priv,
+ eph_pub=self.eph_pub)
+ self.assertEqual(hexlify(z),
+ b"c9532df6aa7e9dbe5fe85da31ee25ff19c179c88691ec4b8328cc2036dcdadf2")
+
+ def test_5(self):
+ # C(2e, 1s) is not supported
+ kdf = lambda x: SHA256.new(x).digest()
+ self.assertRaises(ValueError,
+ key_agreement,
+ kdf=kdf,
+ static_priv=self.static_priv,
+ eph_pub=self.eph_pub,
+ eph_priv=self.eph_priv)
+
+ def test_6(self):
+ # C(2e, 1s) is not supported
+ kdf = lambda x: SHA256.new(x).digest()
+ self.assertRaises(ValueError,
+ key_agreement,
+ kdf=kdf,
+ static_pub=self.static_pub,
+ eph_pub=self.eph_pub,
+ eph_priv=self.eph_priv)
+
+ def test_7(self):
+ # C(2e, 0)
+ kdf = lambda x: SHA256.new(x).digest()
+ z = key_agreement(
+ kdf=kdf,
+ eph_pub=self.eph_pub,
+ eph_priv=self.eph_priv)
+ self.assertEqual(hexlify(z),
+ b"feb257ebe063078b1391aac07913283d7b642ad7df61b46dfc9cd6f420bb896a")
+
+ def test_8(self):
+ # C(1e, 1s)
+ kdf = lambda x: SHA256.new(x).digest()
+ z = key_agreement(
+ kdf=kdf,
+ static_priv=self.static_priv,
+ eph_pub=self.eph_pub)
+ self.assertEqual(hexlify(z),
+ b"ee4dc995117476ed57fd17ff0ed44e9f0466d46b929443bc0db9380317583b04")
+
+ def test_9(self):
+ # C(1e, 1s)
+ kdf = lambda x: SHA256.new(x).digest()
+ z = key_agreement(
+ kdf=kdf,
+ static_pub=self.static_pub,
+ eph_priv=self.eph_priv)
+ self.assertEqual(hexlify(z),
+ b"2351cc2014f7c40468fa072b5d30f706eeaeef7507311cd8e59bab3b43f03c51")
+
+ def test_10(self):
+ # No private (local) keys
+ kdf = lambda x: SHA256.new(x).digest()
+ self.assertRaises(ValueError,
+ key_agreement,
+ kdf=kdf,
+ static_pub=self.static_pub,
+ eph_pub=self.eph_pub)
+
+ def test_11(self):
+ # No public (peer) keys
+ kdf = lambda x: SHA256.new(x).digest()
+ self.assertRaises(ValueError,
+ key_agreement,
+ kdf=kdf,
+ static_priv=self.static_priv,
+ eph_priv=self.eph_priv)
+
+ def test_12(self):
+ # failure if kdf is missing
+ self.assertRaises(ValueError,
+ key_agreement,
+ static_pub=self.static_pub,
+ static_priv=self.static_priv)
+
+
+class X25519_Tests(unittest.TestCase):
+
+ def test_rfc7748_1(self):
+ tvs = (
+ ("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4",
+ "e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c",
+ "c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552"),
+ ("4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d",
+ "e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493",
+ "95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957"),
+ )
+
+ for tv1, tv2, tv3 in tvs:
+ priv_key = DH.import_x25519_private_key(unhexlify(tv1))
+ pub_key = DH.import_x25519_public_key(unhexlify(tv2))
+ result = key_agreement(static_pub=pub_key,
+ static_priv=priv_key,
+ kdf=lambda x: x)
+ self.assertEqual(result, unhexlify(tv3))
+
+ def test_rfc7748_2(self):
+ k = unhexlify("0900000000000000000000000000000000000000000000000000000000000000")
+
+ priv_key = DH.import_x25519_private_key(k)
+ pub_key = DH.import_x25519_public_key(k)
+ result = key_agreement(static_pub=pub_key,
+ static_priv=priv_key,
+ kdf=lambda x: x)
+ self.assertEqual(
+ result,
+ unhexlify("422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079")
+ )
+
+ for _ in range(999):
+ priv_key = DH.import_x25519_private_key(result)
+ pub_key = DH.import_x25519_public_key(k)
+ k = result
+ result = key_agreement(static_pub=pub_key,
+ static_priv=priv_key,
+ kdf=lambda x: x)
+
+ self.assertEqual(
+ result,
+ unhexlify("684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51")
+ )
+
+ def test_rfc7748_3(self):
+ tv1 = "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a"
+ tv2 = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"
+ tv3 = "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb"
+ tv4 = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"
+ tv5 = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"
+
+ alice_priv_key = DH.import_x25519_private_key(unhexlify(tv1))
+ alice_pub_key = DH.import_x25519_public_key(unhexlify(tv2))
+ bob_priv_key = DH.import_x25519_private_key(unhexlify(tv3))
+ bob_pub_key = DH.import_x25519_public_key(unhexlify(tv4))
+ secret = unhexlify(tv5)
+
+ result1 = key_agreement(static_pub=alice_pub_key,
+ static_priv=bob_priv_key,
+ kdf=lambda x: x)
+ result2 = key_agreement(static_pub=bob_pub_key,
+ static_priv=alice_priv_key,
+ kdf=lambda x: x)
+ self.assertEqual(result1, secret)
+ self.assertEqual(result2, secret)
+
+ def test_weak(self):
+
+ weak_keys = (
+ "0000000000000000000000000000000000000000000000000000000000000000",
+ "0100000000000000000000000000000000000000000000000000000000000000",
+ "e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800",
+ "5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157",
+ "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
+ "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
+ "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
+ # The implementation will accept these value, but only because
+ # it will set the MSB to zero (as required by RFC7748, Section 5),
+ # therefore leading to another public key (and to a point which is
+ # not of low order anymore).
+ # "cdeb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880",
+ # "4c9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7",
+ # "d9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ # "daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ # "dbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ )
+
+ for x in weak_keys:
+ self.assertRaises(ValueError,
+ DH.import_x25519_public_key,
+ unhexlify(x))
+
+
+class X448_Tests(unittest.TestCase):
+
+ def test_rfc7748_1(self):
+ tvs = (
+ ("3d262fddf9ec8e88495266fea19a34d28882acef045104d0d1aae121700a779c984c24f8cdd78fbff44943eba368f54b29259a4f1c600ad3",
+ "06fce640fa3487bfda5f6cf2d5263f8aad88334cbd07437f020f08f9814dc031ddbdc38c19c6da2583fa5429db94ada18aa7a7fb4ef8a086",
+ "ce3e4ff95a60dc6697da1db1d85e6afbdf79b50a2412d7546d5f239fe14fbaadeb445fc66a01b0779d98223961111e21766282f73dd96b6f"),
+ ("203d494428b8399352665ddca42f9de8fef600908e0d461cb021f8c538345dd77c3e4806e25f46d3315c44e0a5b4371282dd2c8d5be3095f",
+ "0fbcc2f993cd56d3305b0b7d9e55d4c1a8fb5dbb52f8e9a1e9b6201b165d015894e56c4d3570bee52fe205e28a78b91cdfbde71ce8d157db",
+ "884a02576239ff7a2f2f63b2db6a9ff37047ac13568e1e30fe63c4a7ad1b3ee3a5700df34321d62077e63633c575c1c954514e99da7c179d"),
+ )
+
+ for tv1, tv2, tv3 in tvs:
+ priv_key = DH.import_x448_private_key(unhexlify(tv1))
+ pub_key = DH.import_x448_public_key(unhexlify(tv2))
+ result = key_agreement(static_pub=pub_key,
+ static_priv=priv_key,
+ kdf=lambda x: x)
+ self.assertEqual(result, unhexlify(tv3))
+
+ def test_rfc7748_2(self):
+ k = unhexlify("0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")
+
+ priv_key = DH.import_x448_private_key(k)
+ pub_key = DH.import_x448_public_key(k)
+ result = key_agreement(static_pub=pub_key,
+ static_priv=priv_key,
+ kdf=lambda x: x)
+ self.assertEqual(
+ result,
+ unhexlify("3f482c8a9f19b01e6c46ee9711d9dc14fd4bf67af30765c2ae2b846a4d23a8cd0db897086239492caf350b51f833868b9bc2b3bca9cf4113")
+ )
+
+ for _ in range(999):
+ priv_key = DH.import_x448_private_key(result)
+ pub_key = DH.import_x448_public_key(k)
+ k = result
+ result = key_agreement(static_pub=pub_key,
+ static_priv=priv_key,
+ kdf=lambda x: x)
+
+ self.assertEqual(
+ result,
+ unhexlify("aa3b4749d55b9daf1e5b00288826c467274ce3ebbdd5c17b975e09d4af6c67cf10d087202db88286e2b79fceea3ec353ef54faa26e219f38")
+ )
+
+ def test_rfc7748_3(self):
+ tv1 = "9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b"
+ tv2 = "9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0"
+ tv3 = "1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d"
+ tv4 = "3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609"
+ tv5 = "07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d"
+
+ alice_priv_key = DH.import_x448_private_key(unhexlify(tv1))
+ alice_pub_key = DH.import_x448_public_key(unhexlify(tv2))
+ bob_priv_key = DH.import_x448_private_key(unhexlify(tv3))
+ bob_pub_key = DH.import_x448_public_key(unhexlify(tv4))
+ secret = unhexlify(tv5)
+
+ result1 = key_agreement(static_pub=alice_pub_key,
+ static_priv=bob_priv_key,
+ kdf=lambda x: x)
+ result2 = key_agreement(static_pub=bob_pub_key,
+ static_priv=alice_priv_key,
+ kdf=lambda x: x)
+ self.assertEqual(result1, secret)
+ self.assertEqual(result2, secret)
+
+ def test_weak(self):
+
+ weak_keys = (
+ "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "fefffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "00000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ )
+
+ for x in weak_keys:
+ self.assertRaises(ValueError,
+ DH.import_x448_public_key,
+ unhexlify(x))
+
+
+class TestVectorsX25519Wycheproof(unittest.TestCase):
+
+ desc = "Wycheproof X25519 tests"
+
+ def add_tests_hex(self, filename):
+
+ def encoding(g):
+ return g['type']
+
+ def private(u):
+ return unhexlify(u['private'])
+
+ result = load_test_vectors_wycheproof(("Protocol", "wycheproof"),
+ filename,
+ "Wycheproof ECDH (%s)"
+ % filename,
+ group_tag={'encoding': encoding},
+ unit_tag={'private': private}
+ )
+ self.tv += result
+
+ def add_tests_ascii(self, filename):
+
+ def encoding(g):
+ return g['type']
+
+ def public(u):
+ return u['public']
+
+ def private(u):
+ return u['private']
+
+ result = load_test_vectors_wycheproof(("Protocol", "wycheproof"),
+ filename,
+ "Wycheproof ECDH (%s)"
+ % filename,
+ group_tag={'encoding': encoding},
+ unit_tag={'public': public,
+ 'private': private}
+ )
+ self.tv += result
+
+ def setUp(self):
+ self.tv = []
+ self.desc = None
+
+ self.add_tests_hex("x25519_test.json")
+ self.add_tests_hex("x25519_asn_test.json")
+ self.add_tests_ascii("x25519_pem_test.json")
+ self.add_tests_ascii("x25519_jwk_test.json")
+
+ def shortDescription(self):
+ return self.desc
+
+ def test_verify(self, tv):
+
+ if tv.encoding == "XdhComp":
+ try:
+ public_key = import_x25519_public_key(tv.public)
+ except ValueError as e:
+ assert tv.valid
+ assert tv.warning
+ assert "LowOrderPublic" in tv.flags
+ assert "Invalid Curve25519" in str(e)
+ return
+ private_key = import_x25519_private_key(tv.private)
+ elif tv.encoding in ("XdhAsnComp", "XdhPemComp"):
+ try:
+ public_key = ECC.import_key(tv.public)
+ private_key = ECC.import_key(tv.private)
+ except ECC.UnsupportedEccFeature as e:
+ assert not tv.valid
+ assert "Unsupported ECC" in str(e)
+ return
+ except ValueError:
+ assert tv.valid
+ assert tv.warning
+ assert "LowOrderPublic" in tv.flags
+ return
+ elif tv.encoding == "XdhJwkComp":
+
+ if 'y' in tv.public:
+ return
+ if 'x' not in tv.public:
+ return
+ if 'x' not in tv.private:
+ return
+ if tv.public.get('kty') != 'OKP':
+ return
+ if tv.public.get('crv') != 'X25519':
+ return
+ if tv.private.get('crv') != 'X25519':
+ return
+
+ def base64url_decode(input_str):
+ input_str = input_str.replace('-', '+').replace('_', '/')
+ padding = 4 - (len(input_str) % 4)
+ if padding != 4:
+ input_str += '=' * padding
+ decoded_bytes = base64.b64decode(input_str)
+ return decoded_bytes
+
+ jwk_public = base64url_decode(tv.public['x'])
+ jwk_private = base64url_decode(tv.private['d'])
+
+ try:
+ public_key = import_x25519_public_key(jwk_public)
+ private_key = import_x25519_private_key(jwk_private)
+ except ValueError as e:
+ if tv.valid:
+ assert tv.warning
+ assert "LowOrderPublic" in tv.flags
+ assert "Invalid Curve25519" in str(e)
+ return
+ else:
+ assert "Incorrect length" in str(e)
+ return
+ except ValueError:
+ assert tv.valid
+ else:
+ raise ValueError("Unknown encoding", tv.encoding)
+
+ try:
+ z = key_agreement(static_pub=public_key,
+ static_priv=private_key,
+ kdf=lambda x: x)
+ except ValueError:
+ assert not tv.valid
+ except TypeError as e:
+ assert not tv.valid
+ assert "incompatible curve" in str(e)
+ else:
+ self.assertEqual(z, tv.shared)
+ assert tv.valid
+
+ def runTest(self):
+ for tv in self.tv:
+ self.desc = "Wycheproof XECDH Verify Test #%d (%s, %s)" % (tv.id, tv.comment, tv.filename)
+ self.test_verify(tv)
+
+
+class TestVectorsX448Wycheproof(unittest.TestCase):
+
+ desc = "Wycheproof X448 tests"
+
+ def add_tests_hex(self, filename):
+
+ def encoding(g):
+ return g['type']
+
+ def private(u):
+ return unhexlify(u['private'])
+
+ result = load_test_vectors_wycheproof(("Protocol", "wycheproof"),
+ filename,
+ "Wycheproof ECDH (%s)"
+ % filename,
+ group_tag={'encoding': encoding},
+ unit_tag={'private': private}
+ )
+ self.tv += result
+
+ def add_tests_ascii(self, filename):
+
+ def encoding(g):
+ return g['type']
+
+ def public(u):
+ return u['public']
+
+ def private(u):
+ return u['private']
+
+ result = load_test_vectors_wycheproof(("Protocol", "wycheproof"),
+ filename,
+ "Wycheproof ECDH (%s)"
+ % filename,
+ group_tag={'encoding': encoding},
+ unit_tag={'public': public,
+ 'private': private}
+ )
+ self.tv += result
+
+ def setUp(self):
+ self.tv = []
+ self.desc = None
+
+ self.add_tests_hex("x448_test.json")
+ self.add_tests_hex("x448_asn_test.json")
+ self.add_tests_ascii("x448_pem_test.json")
+ self.add_tests_ascii("x448_jwk_test.json")
+
+ def shortDescription(self):
+ return self.desc
+
+ def test_verify(self, tv):
+
+ if tv.encoding == "XdhComp":
+ try:
+ public_key = import_x448_public_key(tv.public)
+ except ValueError as e:
+ assert tv.valid
+ assert tv.warning
+ if len(tv.public) == 56:
+ assert "LowOrderPublic" in tv.flags
+ assert "Invalid Curve448" in str(e)
+ else:
+ assert "Incorrect Curve448" in str(e)
+ return
+ private_key = import_x448_private_key(tv.private)
+ elif tv.encoding in ("XdhAsnComp", "XdhPemComp"):
+ try:
+ public_key = ECC.import_key(tv.public)
+ private_key = ECC.import_key(tv.private)
+ except ECC.UnsupportedEccFeature as e:
+ assert not tv.valid
+ assert "Unsupported ECC" in str(e)
+ return
+ except ValueError as e:
+ assert tv.valid
+ assert tv.warning
+ assert "LowOrderPublic" in tv.flags or "NonCanonicalPublic" in tv.flags
+ return
+ elif tv.encoding == "XdhJwkComp":
+
+ if 'y' in tv.public:
+ return
+ if 'x' not in tv.public:
+ return
+ if 'x' not in tv.private:
+ return
+ if tv.public.get('kty') != 'OKP':
+ return
+ if tv.public.get('crv') != 'X448':
+ return
+ if tv.private.get('crv') != 'X448':
+ return
+
+ def base64url_decode(input_str):
+ input_str = input_str.replace('-', '+').replace('_', '/')
+ padding = 4 - (len(input_str) % 4)
+ if padding != 4:
+ input_str += '=' * padding
+ decoded_bytes = base64.b64decode(input_str)
+ return decoded_bytes
+
+ jwk_public = base64url_decode(tv.public['x'])
+ jwk_private = base64url_decode(tv.private['d'])
+
+ try:
+ public_key = import_x448_public_key(jwk_public)
+ private_key = import_x448_private_key(jwk_private)
+ except ValueError as e:
+ if tv.valid:
+ assert tv.warning
+ if len(tv.public['x']) == 75:
+ assert "LowOrderPublic" in tv.flags or \
+ "NonCanonicalPublic" in tv.flags
+ assert "Invalid Curve448" in str(e)
+ else:
+ assert "Incorrect Curve448" in str(e)
+ return
+ else:
+ assert "Incorrect length" in str(e)
+ return
+ except ValueError:
+ assert tv.valid
+ else:
+ raise ValueError("Unknown encoding", tv.encoding)
+
+ try:
+ z = key_agreement(static_pub=public_key,
+ static_priv=private_key,
+ kdf=lambda x: x)
+ except ValueError:
+ assert not tv.valid
+ except TypeError as e:
+ assert not tv.valid
+ assert "incompatible curve" in str(e)
+ else:
+ self.assertEqual(z, tv.shared)
+ assert tv.valid
+
+ def runTest(self):
+ for tv in self.tv:
+ self.desc = "Wycheproof XECDH Verify Test #%d (%s, %s)" % (tv.id, tv.comment, tv.filename)
+ self.test_verify(tv)
+
+
+def get_tests(config={}):
+
+ tests = []
+ tests += list_test_cases(FIPS_ECDH_Tests_KAT)
+ tests += [TestVectorsECDHWycheproof()]
+ tests += list_test_cases(ECDH_Tests)
+ tests += list_test_cases(X25519_Tests)
+ tests += list_test_cases(X448_Tests)
+ tests += [TestVectorsX25519Wycheproof()]
+ tests += [TestVectorsX448Wycheproof()]
+
+ slow_tests = config.get('slow_tests')
+ if slow_tests:
+ pass
+
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_rfc1751.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_rfc1751.py
new file mode 100644
index 00000000..0878cc51
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Protocol/test_rfc1751.py
@@ -0,0 +1,62 @@
+#
+# Test script for Crypto.Util.RFC1751.
+#
+# Part of the Python Cryptography Toolkit
+#
+# Written by Andrew Kuchling and others
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+__revision__ = "$Id$"
+
+import binascii
+import unittest
+from Crypto.Util import RFC1751
+from Crypto.Util.py3compat import *
+
+test_data = [('EB33F77EE73D4053', 'TIDE ITCH SLOW REIN RULE MOT'),
+ ('CCAC2AED591056BE4F90FD441C534766',
+ 'RASH BUSH MILK LOOK BAD BRIM AVID GAFF BAIT ROT POD LOVE'),
+ ('EFF81F9BFBC65350920CDD7416DE8009',
+ 'TROD MUTE TAIL WARM CHAR KONG HAAG CITY BORE O TEAL AWL')
+ ]
+
+class RFC1751Test_k2e (unittest.TestCase):
+
+ def runTest (self):
+ "Check converting keys to English"
+ for key, words in test_data:
+ key=binascii.a2b_hex(b(key))
+ self.assertEqual(RFC1751.key_to_english(key), words)
+
+class RFC1751Test_e2k (unittest.TestCase):
+
+ def runTest (self):
+ "Check converting English strings to keys"
+ for key, words in test_data:
+ key=binascii.a2b_hex(b(key))
+ self.assertEqual(RFC1751.english_to_key(words), key)
+
+# class RFC1751Test
+
+def get_tests(config={}):
+ return [RFC1751Test_k2e(), RFC1751Test_e2k()]
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/__init__.py
new file mode 100644
index 00000000..48a445ea
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/__init__.py
@@ -0,0 +1,63 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/PublicKey/__init__.py: Self-test for public key crypto
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test for public-key crypto"""
+
+import unittest
+from Crypto.SelfTest.PublicKey import (test_DSA, test_RSA,
+ test_ECC_NIST,
+ test_ECC_Ed25519,
+ test_ECC_Curve25519,
+ test_ECC_Ed448,
+ test_ECC_Curve448,
+ test_import_DSA, test_import_RSA,
+ test_import_ECC, test_ElGamal,
+ test_import_Curve25519,
+ test_import_Curve448)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += test_DSA.get_tests(config=config)
+ tests += test_RSA.get_tests(config=config)
+ tests += test_ECC_NIST.get_tests(config=config)
+ tests += test_ECC_Ed25519.get_tests(config=config)
+ tests += test_ECC_Curve25519.get_tests(config=config)
+ tests += test_ECC_Ed448.get_tests(config=config)
+ tests += test_ECC_Curve448.get_tests(config=config)
+
+ tests += test_import_DSA.get_tests(config=config)
+ tests += test_import_RSA.get_tests(config=config)
+ tests += test_import_ECC.get_tests(config=config)
+ tests += test_import_Curve25519.get_tests(config=config)
+ tests += test_import_Curve448.get_tests(config=config)
+
+ tests += test_ElGamal.get_tests(config=config)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_DSA.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_DSA.py
new file mode 100644
index 00000000..125cf6c4
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_DSA.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/PublicKey/test_DSA.py: Self-test for the DSA primitive
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.PublicKey.DSA"""
+
+import os
+from Crypto.Util.py3compat import *
+
+import unittest
+from Crypto.SelfTest.st_common import list_test_cases, a2b_hex, b2a_hex
+
+def _sws(s):
+ """Remove whitespace from a text or byte string"""
+ if isinstance(s,str):
+ return "".join(s.split())
+ else:
+ return b("").join(s.split())
+
+class DSATest(unittest.TestCase):
+ # Test vector from "Appendix 5. Example of the DSA" of
+ # "Digital Signature Standard (DSS)",
+ # U.S. Department of Commerce/National Institute of Standards and Technology
+ # FIPS 186-2 (+Change Notice), 2000 January 27.
+ # http://csrc.nist.gov/publications/fips/fips186-2/fips186-2-change1.pdf
+
+ y = _sws("""19131871 d75b1612 a819f29d 78d1b0d7 346f7aa7 7bb62a85
+ 9bfd6c56 75da9d21 2d3a36ef 1672ef66 0b8c7c25 5cc0ec74
+ 858fba33 f44c0669 9630a76b 030ee333""")
+
+ g = _sws("""626d0278 39ea0a13 413163a5 5b4cb500 299d5522 956cefcb
+ 3bff10f3 99ce2c2e 71cb9de5 fa24babf 58e5b795 21925c9c
+ c42e9f6f 464b088c c572af53 e6d78802""")
+
+ p = _sws("""8df2a494 492276aa 3d25759b b06869cb eac0d83a fb8d0cf7
+ cbb8324f 0d7882e5 d0762fc5 b7210eaf c2e9adac 32ab7aac
+ 49693dfb f83724c2 ec0736ee 31c80291""")
+
+ q = _sws("""c773218c 737ec8ee 993b4f2d ed30f48e dace915f""")
+
+ x = _sws("""2070b322 3dba372f de1c0ffc 7b2e3b49 8b260614""")
+
+ k = _sws("""358dad57 1462710f 50e254cf 1a376b2b deaadfbf""")
+ k_inverse = _sws("""0d516729 8202e49b 4116ac10 4fc3f415 ae52f917""")
+ m = b2a_hex(b("abc"))
+ m_hash = _sws("""a9993e36 4706816a ba3e2571 7850c26c 9cd0d89d""")
+ r = _sws("""8bac1ab6 6410435c b7181f95 b16ab97c 92b341c0""")
+ s = _sws("""41e2345f 1f56df24 58f426d1 55b4ba2d b6dcd8c8""")
+
+ def setUp(self):
+ global DSA, Random, bytes_to_long, size
+ from Crypto.PublicKey import DSA
+ from Crypto import Random
+ from Crypto.Util.number import bytes_to_long, inverse, size
+
+ self.dsa = DSA
+
+ def test_generate_1arg(self):
+ """DSA (default implementation) generated key (1 argument)"""
+ dsaObj = self.dsa.generate(1024)
+ self._check_private_key(dsaObj)
+ pub = dsaObj.public_key()
+ self._check_public_key(pub)
+
+ def test_generate_2arg(self):
+ """DSA (default implementation) generated key (2 arguments)"""
+ dsaObj = self.dsa.generate(1024, Random.new().read)
+ self._check_private_key(dsaObj)
+ pub = dsaObj.public_key()
+ self._check_public_key(pub)
+
+ def test_construct_4tuple(self):
+ """DSA (default implementation) constructed key (4-tuple)"""
+ (y, g, p, q) = [bytes_to_long(a2b_hex(param)) for param in (self.y, self.g, self.p, self.q)]
+ dsaObj = self.dsa.construct((y, g, p, q))
+ self._test_verification(dsaObj)
+
+ def test_construct_5tuple(self):
+ """DSA (default implementation) constructed key (5-tuple)"""
+ (y, g, p, q, x) = [bytes_to_long(a2b_hex(param)) for param in (self.y, self.g, self.p, self.q, self.x)]
+ dsaObj = self.dsa.construct((y, g, p, q, x))
+ self._test_signing(dsaObj)
+ self._test_verification(dsaObj)
+
+ def test_construct_bad_key4(self):
+ (y, g, p, q) = [bytes_to_long(a2b_hex(param)) for param in (self.y, self.g, self.p, self.q)]
+ tup = (y, g, p+1, q)
+ self.assertRaises(ValueError, self.dsa.construct, tup)
+
+ tup = (y, g, p, q+1)
+ self.assertRaises(ValueError, self.dsa.construct, tup)
+
+ tup = (y, 1, p, q)
+ self.assertRaises(ValueError, self.dsa.construct, tup)
+
+ def test_construct_bad_key5(self):
+ (y, g, p, q, x) = [bytes_to_long(a2b_hex(param)) for param in (self.y, self.g, self.p, self.q, self.x)]
+ tup = (y, g, p, q, x+1)
+ self.assertRaises(ValueError, self.dsa.construct, tup)
+
+ tup = (y, g, p, q, q+10)
+ self.assertRaises(ValueError, self.dsa.construct, tup)
+
+ def _check_private_key(self, dsaObj):
+ # Check capabilities
+ self.assertEqual(1, dsaObj.has_private())
+ self.assertEqual(1, dsaObj.can_sign())
+ self.assertEqual(0, dsaObj.can_encrypt())
+
+ # Sanity check key data
+ self.assertEqual(1, dsaObj.p > dsaObj.q) # p > q
+ self.assertEqual(160, size(dsaObj.q)) # size(q) == 160 bits
+ self.assertEqual(0, (dsaObj.p - 1) % dsaObj.q) # q is a divisor of p-1
+ self.assertEqual(dsaObj.y, pow(dsaObj.g, dsaObj.x, dsaObj.p)) # y == g**x mod p
+ self.assertEqual(1, 0 < dsaObj.x < dsaObj.q) # 0 < x < q
+
+ def _check_public_key(self, dsaObj):
+ k = bytes_to_long(a2b_hex(self.k))
+ m_hash = bytes_to_long(a2b_hex(self.m_hash))
+
+ # Check capabilities
+ self.assertEqual(0, dsaObj.has_private())
+ self.assertEqual(1, dsaObj.can_sign())
+ self.assertEqual(0, dsaObj.can_encrypt())
+
+ # Check that private parameters are all missing
+ self.assertEqual(0, hasattr(dsaObj, 'x'))
+
+ # Sanity check key data
+ self.assertEqual(1, dsaObj.p > dsaObj.q) # p > q
+ self.assertEqual(160, size(dsaObj.q)) # size(q) == 160 bits
+ self.assertEqual(0, (dsaObj.p - 1) % dsaObj.q) # q is a divisor of p-1
+
+ # Public-only key objects should raise an error when .sign() is called
+ self.assertRaises(TypeError, dsaObj._sign, m_hash, k)
+
+ # Check __eq__ and __ne__
+ self.assertEqual(dsaObj.public_key() == dsaObj.public_key(),True) # assert_
+ self.assertEqual(dsaObj.public_key() != dsaObj.public_key(),False) # assertFalse
+
+ self.assertEqual(dsaObj.public_key(), dsaObj.publickey())
+
+ def _test_signing(self, dsaObj):
+ k = bytes_to_long(a2b_hex(self.k))
+ m_hash = bytes_to_long(a2b_hex(self.m_hash))
+ r = bytes_to_long(a2b_hex(self.r))
+ s = bytes_to_long(a2b_hex(self.s))
+ (r_out, s_out) = dsaObj._sign(m_hash, k)
+ self.assertEqual((r, s), (r_out, s_out))
+
+ def _test_verification(self, dsaObj):
+ m_hash = bytes_to_long(a2b_hex(self.m_hash))
+ r = bytes_to_long(a2b_hex(self.r))
+ s = bytes_to_long(a2b_hex(self.s))
+ self.assertTrue(dsaObj._verify(m_hash, (r, s)))
+ self.assertFalse(dsaObj._verify(m_hash + 1, (r, s)))
+
+ def test_repr(self):
+ (y, g, p, q) = [bytes_to_long(a2b_hex(param)) for param in (self.y, self.g, self.p, self.q)]
+ dsaObj = self.dsa.construct((y, g, p, q))
+ repr(dsaObj)
+
+
+class DSADomainTest(unittest.TestCase):
+
+ def test_domain1(self):
+ """Verify we can generate new keys in a given domain"""
+ dsa_key_1 = DSA.generate(1024)
+ domain_params = dsa_key_1.domain()
+
+ dsa_key_2 = DSA.generate(1024, domain=domain_params)
+ self.assertEqual(dsa_key_1.p, dsa_key_2.p)
+ self.assertEqual(dsa_key_1.q, dsa_key_2.q)
+ self.assertEqual(dsa_key_1.g, dsa_key_2.g)
+
+ self.assertEqual(dsa_key_1.domain(), dsa_key_2.domain())
+
+ def _get_weak_domain(self):
+
+ from Crypto.Math.Numbers import Integer
+ from Crypto.Math import Primality
+
+ p = Integer(4)
+ while p.size_in_bits() != 1024 or Primality.test_probable_prime(p) != Primality.PROBABLY_PRIME:
+ q1 = Integer.random(exact_bits=80)
+ q2 = Integer.random(exact_bits=80)
+ q = q1 * q2
+ z = Integer.random(exact_bits=1024-160)
+ p = z * q + 1
+
+ h = Integer(2)
+ g = 1
+ while g == 1:
+ g = pow(h, z, p)
+ h += 1
+
+ return (p, q, g)
+
+
+ def test_generate_error_weak_domain(self):
+ """Verify that domain parameters with composite q are rejected"""
+
+ domain_params = self._get_weak_domain()
+ self.assertRaises(ValueError, DSA.generate, 1024, domain=domain_params)
+
+
+ def test_construct_error_weak_domain(self):
+ """Verify that domain parameters with composite q are rejected"""
+
+ from Crypto.Math.Numbers import Integer
+
+ p, q, g = self._get_weak_domain()
+ y = pow(g, 89, p)
+ self.assertRaises(ValueError, DSA.construct, (y, g, p, q))
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(DSATest)
+ tests += list_test_cases(DSADomainTest)
+ return tests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Curve25519.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Curve25519.py
new file mode 100644
index 00000000..c637bb37
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Curve25519.py
@@ -0,0 +1,283 @@
+# ===================================================================
+#
+# Copyright (c) 2024, Helder Eijs
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Math.Numbers import Integer
+from Crypto.Hash import SHAKE128
+
+from Crypto.PublicKey import ECC
+from Crypto.PublicKey.ECC import EccKey, EccXPoint, _curves
+
+# Test vectors for scalar multiplication using point with X=9 as base
+# generated with nickovs' Python-only code https://gist.github.com/nickovs/cc3c22d15f239a2640c185035c06f8a3
+# The order is 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed
+# Each tuple is (exponent, X-coordinate)
+scalar_base9_test = [
+ (1, 9),
+ (2, 0x20d342d51873f1b7d9750c687d1571148f3f5ced1e350b5c5cae469cdd684efb),
+ (3, 0x1c12bc1a6d57abe645534d91c21bba64f8824e67621c0859c00a03affb713c12),
+ (4, 0x79ce98b7e0689d7de7d1d074a15b315ffe1805dfcd5d2a230fee85e4550013ef),
+ (6, 0x26954ccdc99ebf34f8f1dde5e6bb080685fec73640494c28f9fe0bfa8c794531),
+ (9, 0x192b929197d07748db44600da41bab7499b1c2e6e2f87c6f0e337980668164ba),
+ (129, 0x7332096a738900085e721103fce2cbf13aee50fef0788ea0d669008eb09ceab7),
+ (255, 0x1534582fc2b1cea45e8cb776547e209da4fd54a9e473b50c5b8c6b0ae023a9b3),
+ (256, 0x4300017536976a742ec8747f7505cd6bc80e610d669acab1a1eed36f680d98e8),
+ (257, 0x6c410611cb484c9016adfb884d37a0e682e075daca1d46f45bb7a4afed10b125),
+ (0x10101, 0xa679e9d7e043bf76c03362576e2c88abe9093c5d4f6b4a202c64a8397467cf),
+ (0xAA55CC, 0x2cc02f84c067e3586f4278326689be163e606d69ccae505bb09488e11f295887),
+ (0x1B29A0E579E0A000567, 0x50c38a72d7bfd7864c8b9083fa123e8d359068e6b491a019a885036e073f6604),
+ (0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed + 1, 9),
+]
+
+
+class TestEccPoint_Curve25519(unittest.TestCase):
+
+ v1 = 0x09fa78b39b00a72930bcd8039be789a0997830bb99f79aeeb93493715390b4e8
+ v2 = 0x15210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493
+
+ def test_init(self):
+ EccXPoint(9, "curve25519")
+ EccXPoint(2**255 - 19 + 5, "curve25519")
+
+ def test_curve_attribute(self):
+ point = EccXPoint(9, "curve25519")
+ self.assertEqual(point.curve, "Curve25519")
+
+ def test_init_fail(self):
+ self.assertRaises(ValueError, EccXPoint, 3*(2**255 - 19), "curve25519")
+ self.assertRaises(ValueError, EccXPoint, 9, "curve25518")
+
+ def test_equal_set(self):
+ point1 = EccXPoint(self.v1, "curve25519")
+ point2 = EccXPoint(self.v2, "curve25519")
+
+ self.assertEqual(point1, point1)
+ self.assertNotEqual(point1, point2)
+
+ point2.set(point1)
+ self.assertEqual(point1.x, point2.x)
+
+ def test_copy(self):
+ point1 = EccXPoint(self.v1, "curve25519")
+ point2 = point1.copy()
+ self.assertEqual(point1.x, point2.x)
+
+ def test_pai(self):
+ point1 = EccXPoint(self.v1, "curve25519")
+ pai = point1.point_at_infinity()
+ self.assertTrue(pai.point_at_infinity())
+
+ point2 = EccXPoint(None, "curve25519")
+ self.assertTrue(point2.point_at_infinity())
+
+ def test_scalar_multiply(self):
+ base = EccXPoint(9, "curve25519")
+
+ pointH = 0 * base
+ self.assertTrue(pointH.point_at_infinity())
+
+ pointH = 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed * base
+ self.assertTrue(pointH.point_at_infinity())
+
+ pointH = base * 1
+ self.assertEqual(pointH.x, 9)
+
+ for d, result in scalar_base9_test:
+ pointH = d * base
+ self.assertEqual(pointH.x, result)
+
+ def test_sizes(self):
+ point = EccXPoint(9, "curve25519")
+ self.assertEqual(point.size_in_bits(), 255)
+ self.assertEqual(point.size_in_bytes(), 32)
+
+
+class TestEccKey_Curve25519(unittest.TestCase):
+
+ def test_private_key(self):
+ # RFC7748 Section 6.1 - Alice
+ alice_priv = unhexlify("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
+ alice_pub = unhexlify("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a")
+ alice_pub_x = Integer.from_bytes(alice_pub, byteorder='little')
+
+ key = EccKey(curve="Curve25519", seed=alice_priv)
+ self.assertEqual(key.seed, alice_priv)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, alice_pub_x)
+
+ # RFC7748 Section 6.1 - Bob
+ bob_priv = unhexlify("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb")
+ bob_pub = unhexlify("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f")
+ bob_pub_x = Integer.from_bytes(bob_pub, byteorder='little')
+
+ key = EccKey(curve="Curve25519", seed=bob_priv)
+ self.assertEqual(key.seed, bob_priv)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, bob_pub_x)
+
+ # Other names
+ key = EccKey(curve="curve25519", seed=alice_priv)
+
+ # Must not accept d parameter
+ self.assertRaises(ValueError, EccKey, curve="curve25519", d=1)
+
+ def test_public_key(self):
+ point = EccXPoint(_curves['curve25519'].Gx,
+ curve='curve25519')
+ key = EccKey(curve="curve25519", point=point)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ def test_public_key_derived(self):
+ priv_key = EccKey(curve="curve25519", seed=b'H'*32)
+ pub_key = priv_key.public_key()
+ self.assertFalse(pub_key.has_private())
+ self.assertEqual(priv_key.pointQ, pub_key.pointQ)
+
+ def test_invalid_seed(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="curve25519", seed=b'H' * 31))
+
+ def test_equality(self):
+ private_key = ECC.construct(seed=b'H'*32, curve="Curve25519")
+ private_key2 = ECC.construct(seed=b'H'*32, curve="curve25519")
+ private_key3 = ECC.construct(seed=b'C'*32, curve="Curve25519")
+
+ public_key = private_key.public_key()
+ public_key2 = private_key2.public_key()
+ public_key3 = private_key3.public_key()
+
+ self.assertEqual(private_key, private_key2)
+ self.assertNotEqual(private_key, private_key3)
+
+ self.assertEqual(public_key, public_key2)
+ self.assertNotEqual(public_key, public_key3)
+
+ self.assertNotEqual(public_key, private_key)
+
+ def test_name_consistency(self):
+ key = ECC.generate(curve='curve25519')
+ self.assertIn("curve='Curve25519'", repr(key))
+ self.assertEqual(key.curve, 'Curve25519')
+ self.assertEqual(key.public_key().curve, 'Curve25519')
+
+
+class TestEccModule_Curve25519(unittest.TestCase):
+
+ def test_generate(self):
+ key = ECC.generate(curve="Curve25519")
+ self.assertTrue(key.has_private())
+ point = EccXPoint(_curves['Curve25519'].Gx, curve="Curve25519") * key.d
+ self.assertEqual(key.pointQ, point)
+
+ # Always random
+ key2 = ECC.generate(curve="Curve25519")
+ self.assertNotEqual(key, key2)
+
+ # Other names
+ ECC.generate(curve="curve25519")
+
+ # Random source
+ key1 = ECC.generate(curve="Curve25519", randfunc=SHAKE128.new().read)
+ key2 = ECC.generate(curve="Curve25519", randfunc=SHAKE128.new().read)
+ self.assertEqual(key1, key2)
+
+ def test_construct(self):
+ seed = unhexlify("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
+ point_hex = unhexlify("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a")
+ Px = Integer.from_bytes(point_hex, byteorder='little')
+ point = EccXPoint(Px, curve="Curve25519")
+
+ # Private key only
+ key = ECC.construct(curve="Curve25519", seed=seed)
+ self.assertEqual(key.pointQ, point)
+ self.assertTrue(key.has_private())
+
+ # Public key only
+ key = ECC.construct(curve="Curve25519", point_x=Px)
+ self.assertEqual(key.pointQ, point)
+ self.assertFalse(key.has_private())
+
+ # Private and public key
+ key = ECC.construct(curve="Curve25519", seed=seed, point_x=Px)
+ self.assertEqual(key.pointQ, point)
+ self.assertTrue(key.has_private())
+
+ # Other names
+ key = ECC.construct(curve="curve25519", seed=seed)
+
+ def test_negative_construct(self):
+ coordG = dict(point_x=_curves['curve25519'].Gx)
+
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519", d=2, **coordG)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519", seed=b'H'*31)
+
+ # Verify you cannot construct weak keys (small-order points)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=0)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=1)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=325606250916557431795983626356110631294008115727848805560023387167927233504)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=39382357235489614581723060781553021112529911719440698176882885853963445705823)
+ p = 2**255 - 19
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=p-1)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=p)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=p+1)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=p+325606250916557431795983626356110631294008115727848805560023387167927233504)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=p+39382357235489614581723060781553021112529911719440698176882885853963445705823)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=p*2-1)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=p*2)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve25519",
+ point_x=p*2+1)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TestEccPoint_Curve25519)
+ tests += list_test_cases(TestEccKey_Curve25519)
+ tests += list_test_cases(TestEccModule_Curve25519)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Curve448.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Curve448.py
new file mode 100644
index 00000000..91da4501
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Curve448.py
@@ -0,0 +1,246 @@
+# This file is licensed under the BSD 2-Clause License.
+# See https://opensource.org/licenses/BSD-2-Clause for details.
+
+import unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Math.Numbers import Integer
+from Crypto.Hash import SHAKE128
+
+from Crypto.PublicKey import ECC
+from Crypto.PublicKey.ECC import EccKey, EccXPoint, _curves
+
+CURVE448_P = 2**448 - 2**224 - 1
+CURVE448_ORDER = 2**446 - 0x8335dc163bb124b65129c96fde933d8d723a70aadc873d6d54a7bb0d
+
+# Test vectors for scalar multiplication using point with X=5 as base
+# Each tuple is (exponent, X-coordinate)
+scalar_base5_test = [
+ (1, 5),
+ (2, 0x6391322257cae3d49aef4665d8bd5cccac9abefb511e83d75f3c766616266fc1bf3747f1da00ed7125e8f0255a1208087d32a4bc1c743cb6),
+ (3, 0x1fbe4b3584cab86170c14b9325840b8a2429b61fb93c42492c002a2807a4e7ea63138ea59bf95652ce9a7d13d0321c7511e3314d0553f34c),
+ (4, 0x93b44a7b78726ba8d0b048bd7144074f8bdad24ef9d0a6c8264f6c00b135ffcea11545e80d18364acc8ebfbcc45358e0da5fd5e5146e2b1),
+ (6, 0x693d165f453bd62871e5e53845f33e9e5b18b24d79c1f9102608aa7ba6f18ac24864012171d64c90b698f5ce5631cd02cee4e4336b1ad88c),
+ (9, 0xb970d576e7d9aa427dbf7cb9b7dd65170721d04ee060c9ea8d499dc361d4cfde1ceb19068eae853bac8f5d92827bdbf3d94c22de2fb42dae),
+ (129, 0x9fbdb50a1450438fe656aa32aa1bb2548d077d5c3a5d327689093a2996a4f94eacd1fb4f90315edb2afe41908a759f0d6db83fa791df80db),
+ (255, 0x31bc3e9385dfd12e1238927061eb0c911466da394e459bf058ba3b08260a258a3c392b0f85ddbd23828657137b88577a85b83774139fab9e),
+ (256, 0x735c7f30e6872e5e4215c0147c8a112d697f668c9bd0f92f5f1e4e6badc128a0b654e697cd4bae2144d54e726b54c1fa63a09b00dd3c17f),
+ (257, 0x95c1b0ce01286dc047aeb5922a5e62b3effb5b9296273a5004eb456f592728dd494a6fb5996a2ea7011ae6423874a48c2927bfa62d8ce8b0),
+ (0x10101, 0x113bb172c9dc52ab45bd665dd9751ed44e33b8596f943c6cb2f8dd329160ece802960b3eb0d2c21ef3a3ac12c20fccbc2a271fc2f061c1b2),
+ (0xAA55CC, 0xcf42585d2e0b1e45c0bfd601c91af4b137d7faf139fc761178c7ded432417c307ee1759af2deec6a14dbaf6b868eb13a6039fbdde4b61898),
+ (0x1B29A0E579E0A000567, 0x7bd9ec9775a664f4d860d82d6be60895113a7c36f92db25583dbba5dc17f09c136ec27e14857bfd6a705311327030aa657dd036325fad330),
+ (CURVE448_ORDER + 1, 5),
+]
+
+
+class TestEccPoint_Curve448(unittest.TestCase):
+
+ v1 = 0x09fa78b39b00a72930bcd8039be789a0997830bb99f79aeeb93493715390b4e8
+ v2 = 0x15210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493
+
+ def test_init(self):
+ EccXPoint(5, "curve448")
+ EccXPoint(CURVE448_P - 5, "curve448")
+
+ def test_curve_attribute(self):
+ point = EccXPoint(5, "curve448")
+ self.assertEqual(point.curve, "Curve448")
+
+ def test_init_fail(self):
+ self.assertRaises(ValueError, EccXPoint, 3*CURVE448_P, "curve448")
+ self.assertRaises(ValueError, EccXPoint, 3, "curve449")
+
+ def test_equal_set(self):
+ point1 = EccXPoint(self.v1, "curve448")
+ point2 = EccXPoint(self.v2, "curve448")
+
+ self.assertEqual(point1, point1)
+ self.assertNotEqual(point1, point2)
+
+ point2.set(point1)
+ self.assertEqual(point1.x, point2.x)
+
+ def test_copy(self):
+ point1 = EccXPoint(self.v1, "curve448")
+ point2 = point1.copy()
+ self.assertEqual(point1.x, point2.x)
+
+ def test_pai(self):
+ point1 = EccXPoint(self.v1, "curve448")
+ pai = point1.point_at_infinity()
+ self.assertTrue(pai.point_at_infinity())
+
+ point2 = EccXPoint(None, "curve448")
+ self.assertTrue(point2.point_at_infinity())
+
+ def test_scalar_multiply(self):
+ base = EccXPoint(5, "curve448")
+
+ pointH = 0 * base
+ self.assertTrue(pointH.point_at_infinity())
+
+ pointH = CURVE448_ORDER * base
+ self.assertTrue(pointH.point_at_infinity())
+
+ pointH = base * 1
+ self.assertEqual(pointH.x, 5)
+
+ for d, result in scalar_base5_test:
+ pointH = d * base
+ self.assertEqual(pointH.x, result)
+
+ def test_sizes(self):
+ point = EccXPoint(5, "curve448")
+ self.assertEqual(point.size_in_bits(), 448)
+ self.assertEqual(point.size_in_bytes(), 56)
+
+
+class TestEccKey_Curve448(unittest.TestCase):
+
+ def test_private_key(self):
+ # RFC7748 Section 6.2 - Alice
+ alice_priv = unhexlify("9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b")
+ alice_pub = unhexlify("9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0")
+ alice_pub_x = Integer.from_bytes(alice_pub, byteorder='little')
+
+ key = EccKey(curve="Curve448", seed=alice_priv)
+ self.assertEqual(key.seed, alice_priv)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, alice_pub_x)
+
+ # RFC7748 Section 6.2 - Bob
+ bob_priv = unhexlify("1c306a7ac2a0e2e0990b294470cba339e6453772b075811d8fad0d1d6927c120bb5ee8972b0d3e21374c9c921b09d1b0366f10b65173992d")
+ bob_pub = unhexlify("3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609")
+ bob_pub_x = Integer.from_bytes(bob_pub, byteorder='little')
+
+ key = EccKey(curve="Curve448", seed=bob_priv)
+ self.assertEqual(key.seed, bob_priv)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, bob_pub_x)
+
+ # Other names
+ key = EccKey(curve="curve448", seed=alice_priv)
+
+ # Must not accept d parameter
+ self.assertRaises(ValueError, EccKey, curve="curve448", d=1)
+
+ def test_public_key(self):
+ point = EccXPoint(_curves['curve448'].Gx,
+ curve='curve448')
+ key = EccKey(curve="curve448", point=point)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ def test_public_key_derived(self):
+ priv_key = EccKey(curve="curve448", seed=b'H'*56)
+ pub_key = priv_key.public_key()
+ self.assertFalse(pub_key.has_private())
+ self.assertEqual(priv_key.pointQ, pub_key.pointQ)
+
+ def test_invalid_seed(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="curve448",
+ seed=b'H' * 55))
+
+ def test_equality(self):
+ private_key = ECC.construct(seed=b'H'*56, curve="Curve448")
+ private_key2 = ECC.construct(seed=b'H'*56, curve="curve448")
+ private_key3 = ECC.construct(seed=b'C'*56, curve="Curve448")
+
+ public_key = private_key.public_key()
+ public_key2 = private_key2.public_key()
+ public_key3 = private_key3.public_key()
+
+ self.assertEqual(private_key, private_key2)
+ self.assertNotEqual(private_key, private_key3)
+
+ self.assertEqual(public_key, public_key2)
+ self.assertNotEqual(public_key, public_key3)
+
+ self.assertNotEqual(public_key, private_key)
+
+ def test_name_consistency(self):
+ key = ECC.generate(curve='curve448')
+ self.assertIn("curve='Curve448'", repr(key))
+ self.assertEqual(key.curve, 'Curve448')
+ self.assertEqual(key.public_key().curve, 'Curve448')
+
+
+class TestEccModule_Curve448(unittest.TestCase):
+
+ def test_generate(self):
+ key = ECC.generate(curve="Curve448")
+ self.assertTrue(key.has_private())
+ point = EccXPoint(_curves['Curve448'].Gx, curve="Curve448") * key.d
+ self.assertEqual(key.pointQ, point)
+
+ # Always random
+ key2 = ECC.generate(curve="Curve448")
+ self.assertNotEqual(key, key2)
+
+ # Other names
+ ECC.generate(curve="curve448")
+
+ # Random source
+ key1 = ECC.generate(curve="Curve448", randfunc=SHAKE128.new().read)
+ key2 = ECC.generate(curve="Curve448", randfunc=SHAKE128.new().read)
+ self.assertEqual(key1, key2)
+
+ def test_construct(self):
+ seed = unhexlify("9a8f4925d1519f5775cf46b04b5800d4ee9ee8bae8bc5565d498c28dd9c9baf574a9419744897391006382a6f127ab1d9ac2d8c0a598726b")
+ point_hex = unhexlify("9b08f7cc31b7e3e67d22d5aea121074a273bd2b83de09c63faa73d2c22c5d9bbc836647241d953d40c5b12da88120d53177f80e532c41fa0")
+ Px = Integer.from_bytes(point_hex, byteorder='little')
+ point = EccXPoint(Px, curve="Curve448")
+
+ # Private key only
+ key = ECC.construct(curve="Curve448", seed=seed)
+ self.assertEqual(key.pointQ, point)
+ self.assertTrue(key.has_private())
+
+ # Public key only
+ key = ECC.construct(curve="Curve448", point_x=Px)
+ self.assertEqual(key.pointQ, point)
+ self.assertFalse(key.has_private())
+
+ # Private and public key
+ key = ECC.construct(curve="Curve448", seed=seed, point_x=Px)
+ self.assertEqual(key.pointQ, point)
+ self.assertTrue(key.has_private())
+
+ # Other names
+ key = ECC.construct(curve="curve448", seed=seed)
+
+ def test_negative_construct(self):
+ coordG = dict(point_x=_curves['curve448'].Gx)
+
+ self.assertRaises(ValueError, ECC.construct, curve="Curve448",
+ d=2, **coordG)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve448",
+ seed=b'H'*55)
+
+ # Verify you cannot construct weak keys (small-order points)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve448",
+ point_x=0)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve448",
+ point_x=1)
+ p = 2**448 - 2**224 - 1
+ self.assertRaises(ValueError, ECC.construct, curve="Curve448",
+ point_x=p-1)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve448",
+ point_x=p)
+ self.assertRaises(ValueError, ECC.construct, curve="Curve448",
+ point_x=p+1)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TestEccPoint_Curve448)
+ tests += list_test_cases(TestEccKey_Curve448)
+ tests += list_test_cases(TestEccModule_Curve448)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Ed25519.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Ed25519.py
new file mode 100644
index 00000000..c79b05c2
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Ed25519.py
@@ -0,0 +1,341 @@
+# ===================================================================
+#
+# Copyright (c) 2022, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors
+
+from Crypto.PublicKey import ECC
+from Crypto.PublicKey.ECC import EccPoint, _curves, EccKey
+
+from Crypto.Math.Numbers import Integer
+
+from Crypto.Hash import SHAKE128
+
+
+class TestEccPoint_Ed25519(unittest.TestCase):
+
+ Gxy = {"x": 15112221349535400772501151409588531511454012693041857206046113283949847762202,
+ "y": 46316835694926478169428394003475163141307993866256225615783033603165251855960}
+
+ G2xy = {"x": 24727413235106541002554574571675588834622768167397638456726423682521233608206,
+ "y": 15549675580280190176352668710449542251549572066445060580507079593062643049417}
+
+ G3xy = {"x": 46896733464454938657123544595386787789046198280132665686241321779790909858396,
+ "y": 8324843778533443976490377120369201138301417226297555316741202210403726505172}
+
+ pointG = EccPoint(Gxy['x'], Gxy['y'], curve="ed25519")
+ pointG2 = EccPoint(G2xy['x'], G2xy['y'], curve="ed25519")
+ pointG3 = EccPoint(G3xy['x'], G3xy['y'], curve="ed25519")
+
+ def test_curve_attribute(self):
+ self.assertEqual(self.pointG.curve, "Ed25519")
+
+ def test_init_xy(self):
+ EccPoint(self.Gxy['x'], self.Gxy['y'], curve="Ed25519")
+
+ # Neutral point
+ pai = EccPoint(0, 1, curve="Ed25519")
+ self.assertEqual(pai.x, 0)
+ self.assertEqual(pai.y, 1)
+ self.assertEqual(pai.xy, (0, 1))
+
+ # G
+ bp = self.pointG.copy()
+ self.assertEqual(bp.x, 15112221349535400772501151409588531511454012693041857206046113283949847762202)
+ self.assertEqual(bp.y, 46316835694926478169428394003475163141307993866256225615783033603165251855960)
+ self.assertEqual(bp.xy, (bp.x, bp.y))
+
+ # 2G
+ bp2 = self.pointG2.copy()
+ self.assertEqual(bp2.x, 24727413235106541002554574571675588834622768167397638456726423682521233608206)
+ self.assertEqual(bp2.y, 15549675580280190176352668710449542251549572066445060580507079593062643049417)
+ self.assertEqual(bp2.xy, (bp2.x, bp2.y))
+
+ # 5G
+ EccPoint(x=33467004535436536005251147249499675200073690106659565782908757308821616914995,
+ y=43097193783671926753355113395909008640284023746042808659097434958891230611693,
+ curve="Ed25519")
+
+ # Catch if point is not on the curve
+ self.assertRaises(ValueError, EccPoint, 34, 35, curve="Ed25519")
+
+ def test_set(self):
+ pointW = EccPoint(0, 1, curve="Ed25519")
+ pointW.set(self.pointG)
+ self.assertEqual(pointW.x, self.pointG.x)
+ self.assertEqual(pointW.y, self.pointG.y)
+
+ def test_copy(self):
+ pointW = self.pointG.copy()
+ self.assertEqual(pointW.x, self.pointG.x)
+ self.assertEqual(pointW.y, self.pointG.y)
+
+ def test_equal(self):
+ pointH = self.pointG.copy()
+ pointI = self.pointG2.copy()
+ self.assertEqual(self.pointG, pointH)
+ self.assertNotEqual(self.pointG, pointI)
+
+ def test_pai(self):
+ pai = EccPoint(0, 1, curve="Ed25519")
+ self.assertTrue(pai.is_point_at_infinity())
+ self.assertEqual(pai, pai.point_at_infinity())
+
+ def test_negate(self):
+ negG = -self.pointG
+ G100 = self.pointG * 100
+ sum_zero = G100 + negG * 100
+ self.assertTrue(sum_zero.is_point_at_infinity())
+
+ sum_99 = G100 + negG
+ expected = self.pointG * 99
+ self.assertEqual(sum_99, expected)
+
+ def test_addition(self):
+ self.assertEqual(self.pointG + self.pointG2, self.pointG3)
+ self.assertEqual(self.pointG2 + self.pointG, self.pointG3)
+ self.assertEqual(self.pointG2 + self.pointG.point_at_infinity(), self.pointG2)
+ self.assertEqual(self.pointG.point_at_infinity() + self.pointG2, self.pointG2)
+
+ G5 = self.pointG2 + self.pointG3
+ self.assertEqual(G5.x, 33467004535436536005251147249499675200073690106659565782908757308821616914995)
+ self.assertEqual(G5.y, 43097193783671926753355113395909008640284023746042808659097434958891230611693)
+
+ def test_inplace_addition(self):
+ pointH = self.pointG.copy()
+ pointH += self.pointG
+ self.assertEqual(pointH, self.pointG2)
+ pointH += self.pointG
+ self.assertEqual(pointH, self.pointG3)
+ pointH += self.pointG.point_at_infinity()
+ self.assertEqual(pointH, self.pointG3)
+
+ def test_doubling(self):
+ pointH = self.pointG.copy()
+ pointH.double()
+ self.assertEqual(pointH.x, self.pointG2.x)
+ self.assertEqual(pointH.y, self.pointG2.y)
+
+ # 2*0
+ pai = self.pointG.point_at_infinity()
+ pointR = pai.copy()
+ pointR.double()
+ self.assertEqual(pointR, pai)
+
+ def test_scalar_multiply(self):
+ d = 0
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 0)
+ self.assertEqual(pointH.y, 1)
+
+ d = 1
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, self.pointG.x)
+ self.assertEqual(pointH.y, self.pointG.y)
+
+ d = 2
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, self.pointG2.x)
+ self.assertEqual(pointH.y, self.pointG2.y)
+
+ d = 3
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, self.pointG3.x)
+ self.assertEqual(pointH.y, self.pointG3.y)
+
+ d = 4
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 14582954232372986451776170844943001818709880559417862259286374126315108956272)
+ self.assertEqual(pointH.y, 32483318716863467900234833297694612235682047836132991208333042722294373421359)
+
+ d = 5
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 33467004535436536005251147249499675200073690106659565782908757308821616914995)
+ self.assertEqual(pointH.y, 43097193783671926753355113395909008640284023746042808659097434958891230611693)
+
+ d = 10
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 43500613248243327786121022071801015118933854441360174117148262713429272820047)
+ self.assertEqual(pointH.y, 45005105423099817237495816771148012388779685712352441364231470781391834741548)
+
+ d = 20
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 46694936775300686710656303283485882876784402425210400817529601134760286812591)
+ self.assertEqual(pointH.y, 8786390172762935853260670851718824721296437982862763585171334833968259029560)
+
+ d = 255
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 36843863416400016952258312492144504209624961884991522125275155377549541182230)
+ self.assertEqual(pointH.y, 22327030283879720808995671630924669697661065034121040761798775626517750047180)
+
+ d = 256
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 42740085206947573681423002599456489563927820004573071834350074001818321593686)
+ self.assertEqual(pointH.y, 6935684722522267618220753829624209639984359598320562595061366101608187623111)
+
+ def test_sizes(self):
+ self.assertEqual(self.pointG.size_in_bits(), 255)
+ self.assertEqual(self.pointG.size_in_bytes(), 32)
+
+
+class TestEccKey_Ed25519(unittest.TestCase):
+
+ def test_private_key(self):
+ seed = unhexlify("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
+ Px = 38815646466658113194383306759739515082307681141926459231621296960732224964046
+ Py = 11903303657706407974989296177215005343713679411332034699907763981919547054807
+
+ key = EccKey(curve="Ed25519", seed=seed)
+ self.assertEqual(key.seed, seed)
+ self.assertEqual(key.d, 36144925721603087658594284515452164870581325872720374094707712194495455132720)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, Px)
+ self.assertEqual(key.pointQ.y, Py)
+
+ point = EccPoint(Px, Py, "ed25519")
+ key = EccKey(curve="Ed25519", seed=seed, point=point)
+ self.assertEqual(key.d, 36144925721603087658594284515452164870581325872720374094707712194495455132720)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ # Other names
+ key = EccKey(curve="ed25519", seed=seed)
+
+ # Must not accept d parameter
+ self.assertRaises(ValueError, EccKey, curve="ed25519", d=1)
+
+ def test_public_key(self):
+ point = EccPoint(_curves['ed25519'].Gx, _curves['ed25519'].Gy, curve='ed25519')
+ key = EccKey(curve="ed25519", point=point)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ def test_public_key_derived(self):
+ priv_key = EccKey(curve="ed25519", seed=b'H'*32)
+ pub_key = priv_key.public_key()
+ self.assertFalse(pub_key.has_private())
+ self.assertEqual(priv_key.pointQ, pub_key.pointQ)
+
+ def test_invalid_seed(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="ed25519", seed=b'H' * 31))
+
+ def test_equality(self):
+ private_key = ECC.construct(seed=b'H'*32, curve="Ed25519")
+ private_key2 = ECC.construct(seed=b'H'*32, curve="ed25519")
+ private_key3 = ECC.construct(seed=b'C'*32, curve="Ed25519")
+
+ public_key = private_key.public_key()
+ public_key2 = private_key2.public_key()
+ public_key3 = private_key3.public_key()
+
+ self.assertEqual(private_key, private_key2)
+ self.assertNotEqual(private_key, private_key3)
+
+ self.assertEqual(public_key, public_key2)
+ self.assertNotEqual(public_key, public_key3)
+
+ self.assertNotEqual(public_key, private_key)
+
+ def test_name_consistency(self):
+ key = ECC.generate(curve='ed25519')
+ self.assertIn("curve='Ed25519'", repr(key))
+ self.assertEqual(key.curve, 'Ed25519')
+ self.assertEqual(key.public_key().curve, 'Ed25519')
+
+
+class TestEccModule_Ed25519(unittest.TestCase):
+
+ def test_generate(self):
+ key = ECC.generate(curve="Ed25519")
+ self.assertTrue(key.has_private())
+ point = EccPoint(_curves['Ed25519'].Gx, _curves['Ed25519'].Gy, curve="Ed25519") * key.d
+ self.assertEqual(key.pointQ, point)
+
+ # Always random
+ key2 = ECC.generate(curve="Ed25519")
+ self.assertNotEqual(key, key2)
+
+ # Other names
+ ECC.generate(curve="Ed25519")
+
+ # Random source
+ key1 = ECC.generate(curve="Ed25519", randfunc=SHAKE128.new().read)
+ key2 = ECC.generate(curve="Ed25519", randfunc=SHAKE128.new().read)
+ self.assertEqual(key1, key2)
+
+ def test_construct(self):
+ seed = unhexlify("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
+ Px = 38815646466658113194383306759739515082307681141926459231621296960732224964046
+ Py = 11903303657706407974989296177215005343713679411332034699907763981919547054807
+ d = 36144925721603087658594284515452164870581325872720374094707712194495455132720
+ point = EccPoint(Px, Py, curve="Ed25519")
+
+ # Private key only
+ key = ECC.construct(curve="Ed25519", seed=seed)
+ self.assertEqual(key.pointQ, point)
+ self.assertTrue(key.has_private())
+
+ # Public key only
+ key = ECC.construct(curve="Ed25519", point_x=Px, point_y=Py)
+ self.assertEqual(key.pointQ, point)
+ self.assertFalse(key.has_private())
+
+ # Private and public key
+ key = ECC.construct(curve="Ed25519", seed=seed, point_x=Px, point_y=Py)
+ self.assertEqual(key.pointQ, point)
+ self.assertTrue(key.has_private())
+
+ # Other names
+ key = ECC.construct(curve="ed25519", seed=seed)
+
+ def test_negative_construct(self):
+ coord = dict(point_x=10, point_y=4)
+ coordG = dict(point_x=_curves['ed25519'].Gx, point_y=_curves['ed25519'].Gy)
+
+ self.assertRaises(ValueError, ECC.construct, curve="Ed25519", **coord)
+ self.assertRaises(ValueError, ECC.construct, curve="Ed25519", d=2, **coordG)
+ self.assertRaises(ValueError, ECC.construct, curve="Ed25519", seed=b'H'*31)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TestEccPoint_Ed25519)
+ tests += list_test_cases(TestEccKey_Ed25519)
+ tests += list_test_cases(TestEccModule_Ed25519)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Ed448.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Ed448.py
new file mode 100644
index 00000000..5977287d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_Ed448.py
@@ -0,0 +1,336 @@
+# ===================================================================
+#
+# Copyright (c) 2022, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors
+
+from Crypto.PublicKey import ECC
+from Crypto.PublicKey.ECC import EccPoint, _curves, EccKey
+
+from Crypto.Math.Numbers import Integer
+
+from Crypto.Hash import SHAKE128
+
+
+class TestEccPoint_Ed448(unittest.TestCase):
+
+ Gxy = {"x": 0x4f1970c66bed0ded221d15a622bf36da9e146570470f1767ea6de324a3d3a46412ae1af72ab66511433b80e18b00938e2626a82bc70cc05e,
+ "y": 0x693f46716eb6bc248876203756c9c7624bea73736ca3984087789c1e05a0c2d73ad3ff1ce67c39c4fdbd132c4ed7c8ad9808795bf230fa14}
+
+ G2xy = {"x": 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa955555555555555555555555555555555555555555555555555555555,
+ "y": 0xae05e9634ad7048db359d6205086c2b0036ed7a035884dd7b7e36d728ad8c4b80d6565833a2a3098bbbcb2bed1cda06bdaeafbcdea9386ed}
+
+ G3xy = {"x": 0x865886b9108af6455bd64316cb6943332241b8b8cda82c7e2ba077a4a3fcfe8daa9cbf7f6271fd6e862b769465da8575728173286ff2f8f,
+ "y": 0xe005a8dbd5125cf706cbda7ad43aa6449a4a8d952356c3b9fce43c82ec4e1d58bb3a331bdb6767f0bffa9a68fed02dafb822ac13588ed6fc}
+
+ pointG = EccPoint(Gxy['x'], Gxy['y'], curve="ed448")
+ pointG2 = EccPoint(G2xy['x'], G2xy['y'], curve="ed448")
+ pointG3 = EccPoint(G3xy['x'], G3xy['y'], curve="ed448")
+
+ def test_curve_attribute(self):
+ self.assertEqual(self.pointG.curve, "Ed448")
+
+ def test_init_xy(self):
+ EccPoint(self.Gxy['x'], self.Gxy['y'], curve="Ed448")
+
+ # Neutral point
+ pai = EccPoint(0, 1, curve="Ed448")
+ self.assertEqual(pai.x, 0)
+ self.assertEqual(pai.y, 1)
+ self.assertEqual(pai.xy, (0, 1))
+
+ # G
+ bp = self.pointG.copy()
+ self.assertEqual(bp.x, 0x4f1970c66bed0ded221d15a622bf36da9e146570470f1767ea6de324a3d3a46412ae1af72ab66511433b80e18b00938e2626a82bc70cc05e)
+ self.assertEqual(bp.y, 0x693f46716eb6bc248876203756c9c7624bea73736ca3984087789c1e05a0c2d73ad3ff1ce67c39c4fdbd132c4ed7c8ad9808795bf230fa14)
+ self.assertEqual(bp.xy, (bp.x, bp.y))
+
+ # 2G
+ bp2 = self.pointG2.copy()
+ self.assertEqual(bp2.x, 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa955555555555555555555555555555555555555555555555555555555)
+ self.assertEqual(bp2.y, 0xae05e9634ad7048db359d6205086c2b0036ed7a035884dd7b7e36d728ad8c4b80d6565833a2a3098bbbcb2bed1cda06bdaeafbcdea9386ed)
+ self.assertEqual(bp2.xy, (bp2.x, bp2.y))
+
+ # 5G
+ EccPoint(x=0x7a9f9335a48dcb0e2ba7601eedb50def80cbcf728562ada756d761e8958812808bc0d57a920c3c96f07b2d8cefc6f950d0a99d1092030034,
+ y=0xadfd751a2517edd3b9109ce4fd580ade260ca1823ab18fced86551f7b698017127d7a4ee59d2b33c58405512881f225443b4731472f435eb,
+ curve="Ed448")
+
+ # Catch if point is not on the curve
+ self.assertRaises(ValueError, EccPoint, 34, 35, curve="Ed448")
+
+ def test_set(self):
+ pointW = EccPoint(0, 1, curve="Ed448")
+ pointW.set(self.pointG)
+ self.assertEqual(pointW.x, self.pointG.x)
+ self.assertEqual(pointW.y, self.pointG.y)
+
+ def test_copy(self):
+ pointW = self.pointG.copy()
+ self.assertEqual(pointW.x, self.pointG.x)
+ self.assertEqual(pointW.y, self.pointG.y)
+
+ def test_equal(self):
+ pointH = self.pointG.copy()
+ pointI = self.pointG2.copy()
+ self.assertEqual(self.pointG, pointH)
+ self.assertNotEqual(self.pointG, pointI)
+
+ def test_pai(self):
+ pai = EccPoint(0, 1, curve="Ed448")
+ self.assertTrue(pai.is_point_at_infinity())
+ self.assertEqual(pai, pai.point_at_infinity())
+
+ def test_negate(self):
+ negG = -self.pointG
+ sum = self.pointG + negG
+ self.assertTrue(sum.is_point_at_infinity())
+
+ def test_addition(self):
+ self.assertEqual(self.pointG + self.pointG2, self.pointG3)
+ self.assertEqual(self.pointG2 + self.pointG, self.pointG3)
+ self.assertEqual(self.pointG2 + self.pointG.point_at_infinity(), self.pointG2)
+ self.assertEqual(self.pointG.point_at_infinity() + self.pointG2, self.pointG2)
+
+ G5 = self.pointG2 + self.pointG3
+ self.assertEqual(G5.x, 0x7a9f9335a48dcb0e2ba7601eedb50def80cbcf728562ada756d761e8958812808bc0d57a920c3c96f07b2d8cefc6f950d0a99d1092030034)
+ self.assertEqual(G5.y, 0xadfd751a2517edd3b9109ce4fd580ade260ca1823ab18fced86551f7b698017127d7a4ee59d2b33c58405512881f225443b4731472f435eb)
+
+ def test_inplace_addition(self):
+ pointH = self.pointG.copy()
+ pointH += self.pointG
+ self.assertEqual(pointH, self.pointG2)
+ pointH += self.pointG
+ self.assertEqual(pointH, self.pointG3)
+ pointH += self.pointG.point_at_infinity()
+ self.assertEqual(pointH, self.pointG3)
+
+ def test_doubling(self):
+ pointH = self.pointG.copy()
+ pointH.double()
+ self.assertEqual(pointH.x, self.pointG2.x)
+ self.assertEqual(pointH.y, self.pointG2.y)
+
+ # 2*0
+ pai = self.pointG.point_at_infinity()
+ pointR = pai.copy()
+ pointR.double()
+ self.assertEqual(pointR, pai)
+
+ def test_scalar_multiply(self):
+ d = 0
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 0)
+ self.assertEqual(pointH.y, 1)
+
+ d = 1
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, self.pointG.x)
+ self.assertEqual(pointH.y, self.pointG.y)
+
+ d = 2
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, self.pointG2.x)
+ self.assertEqual(pointH.y, self.pointG2.y)
+
+ d = 3
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, self.pointG3.x)
+ self.assertEqual(pointH.y, self.pointG3.y)
+
+ d = 4
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 0x49dcbc5c6c0cce2c1419a17226f929ea255a09cf4e0891c693fda4be70c74cc301b7bdf1515dd8ba21aee1798949e120e2ce42ac48ba7f30)
+ self.assertEqual(pointH.y, 0xd49077e4accde527164b33a5de021b979cb7c02f0457d845c90dc3227b8a5bc1c0d8f97ea1ca9472b5d444285d0d4f5b32e236f86de51839)
+
+ d = 5
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 0x7a9f9335a48dcb0e2ba7601eedb50def80cbcf728562ada756d761e8958812808bc0d57a920c3c96f07b2d8cefc6f950d0a99d1092030034)
+ self.assertEqual(pointH.y, 0xadfd751a2517edd3b9109ce4fd580ade260ca1823ab18fced86551f7b698017127d7a4ee59d2b33c58405512881f225443b4731472f435eb)
+
+ d = 10
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 0x77486f9d19f6411cdd35d30d1c3235f71936452c787e5c034134d3e8172278aca61622bc805761ce3dab65118a0122d73b403165d0ed303d)
+ self.assertEqual(pointH.y, 0x4d2fea0b026be11024f1f0fe7e94e618e8ac17381ada1d1bf7ee293a68ff5d0bf93c1997dc1aabdc0c7e6381428d85b6b1954a89e4cddf67)
+
+ d = 20
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 0x3c236422354600fe6763defcc1503737e4ed89e262d0de3ec1e552020f2a56fe3b9e1e012d021072598c3c2821e18268bb8fb8339c0d1216)
+ self.assertEqual(pointH.y, 0xb555b9721f630ccb05fc466de4c74d3d2781e69eca88e1b040844f04cab39fd946f91c688fa42402bb38fb9c3e61231017020b219b4396e1)
+
+ d = 255
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 0xbeb7f8388b05cd9c1aa2e3c0dcf31e2b563659361826225390e7748654f627d5c36cbe627e9019936b56d15d4dad7c337c09bac64ff4197f)
+ self.assertEqual(pointH.y, 0x1e37312b2dd4e9440c43c6e7725fc4fa3d11e582d4863f1d018e28f50c0efdb1f53f9b01ada7c87fa162b1f0d72401015d57613d25f1ad53)
+
+ d = 256
+ pointH = d * self.pointG
+ self.assertEqual(pointH.x, 0xf19c34feb56730e3e2be761ac0a2a2b24853b281dda019fc35a5ab58e3696beb39609ae756b0d20fb7ccf0d79aaf5f3bca2e4fdb25bfac1c)
+ self.assertEqual(pointH.y, 0x3beb69cc9111bffcaddc61d363ce6fe5dd44da4aadce78f52e92e985d5442344ced72c4611ed0daac9f4f5661eab73d7a12d25ce8a30241e)
+
+ def test_sizes(self):
+ self.assertEqual(self.pointG.size_in_bits(), 448)
+ self.assertEqual(self.pointG.size_in_bytes(), 56)
+
+
+class TestEccKey_Ed448(unittest.TestCase):
+
+ def test_private_key(self):
+ seed = unhexlify("4adf5d37ac6785e83e99a924f92676d366a78690af59c92b6bdf14f9cdbcf26fdad478109607583d633b60078d61d51d81b7509c5433b0d4c9")
+ Px = 0x72a01eea003a35f9ac44231dc4aae2a382f351d80bf32508175b0855edcf389aa2bbf308dd961ce361a6e7c2091bc78957f6ebcf3002a617
+ Py = 0x9e0d08d84586e9aeefecacb41d049b831f1a3ee0c3eada63e34557b30702b50ab59fb372feff7c30b8cbb7dd51afbe88444ec56238722ec1
+
+ key = EccKey(curve="Ed448", seed=seed)
+ self.assertEqual(key.seed, seed)
+ self.assertEqual(key.d, 0xb07cf179604f83433186e5178760c759c15125ee54ff6f8dcde46e872b709ac82ed0bd0a4e036d774034dcb18a9fb11894657a1485895f80)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, Px)
+ self.assertEqual(key.pointQ.y, Py)
+
+ point = EccPoint(Px, Py, "ed448")
+ key = EccKey(curve="Ed448", seed=seed, point=point)
+ self.assertEqual(key.d, 0xb07cf179604f83433186e5178760c759c15125ee54ff6f8dcde46e872b709ac82ed0bd0a4e036d774034dcb18a9fb11894657a1485895f80)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ # Other names
+ key = EccKey(curve="ed448", seed=seed)
+
+ # Must not accept d parameter
+ self.assertRaises(ValueError, EccKey, curve="ed448", d=1)
+
+ def test_public_key(self):
+ point = EccPoint(_curves['ed448'].Gx, _curves['ed448'].Gy, curve='ed448')
+ key = EccKey(curve="ed448", point=point)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ def test_public_key_derived(self):
+ priv_key = EccKey(curve="ed448", seed=b'H'*57)
+ pub_key = priv_key.public_key()
+ self.assertFalse(pub_key.has_private())
+ self.assertEqual(priv_key.pointQ, pub_key.pointQ)
+
+ def test_invalid_seed(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="ed448", seed=b'H' * 56))
+
+ def test_equality(self):
+ private_key = ECC.construct(seed=b'H'*57, curve="Ed448")
+ private_key2 = ECC.construct(seed=b'H'*57, curve="ed448")
+ private_key3 = ECC.construct(seed=b'C'*57, curve="Ed448")
+
+ public_key = private_key.public_key()
+ public_key2 = private_key2.public_key()
+ public_key3 = private_key3.public_key()
+
+ self.assertEqual(private_key, private_key2)
+ self.assertNotEqual(private_key, private_key3)
+
+ self.assertEqual(public_key, public_key2)
+ self.assertNotEqual(public_key, public_key3)
+
+ self.assertNotEqual(public_key, private_key)
+
+ def test_name_consistency(self):
+ key = ECC.generate(curve='ed448')
+ self.assertIn("curve='Ed448'", repr(key))
+ self.assertEqual(key.curve, 'Ed448')
+ self.assertEqual(key.public_key().curve, 'Ed448')
+
+
+class TestEccModule_Ed448(unittest.TestCase):
+
+ def test_generate(self):
+ key = ECC.generate(curve="Ed448")
+ self.assertTrue(key.has_private())
+ point = EccPoint(_curves['Ed448'].Gx, _curves['Ed448'].Gy, curve="Ed448") * key.d
+ self.assertEqual(key.pointQ, point)
+
+ # Always random
+ key2 = ECC.generate(curve="Ed448")
+ self.assertNotEqual(key, key2)
+
+ # Other names
+ ECC.generate(curve="Ed448")
+
+ # Random source
+ key1 = ECC.generate(curve="Ed448", randfunc=SHAKE128.new().read)
+ key2 = ECC.generate(curve="Ed448", randfunc=SHAKE128.new().read)
+ self.assertEqual(key1, key2)
+
+ def test_construct(self):
+ seed = unhexlify("4adf5d37ac6785e83e99a924f92676d366a78690af59c92b6bdf14f9cdbcf26fdad478109607583d633b60078d61d51d81b7509c5433b0d4c9")
+ Px = 0x72a01eea003a35f9ac44231dc4aae2a382f351d80bf32508175b0855edcf389aa2bbf308dd961ce361a6e7c2091bc78957f6ebcf3002a617
+ Py = 0x9e0d08d84586e9aeefecacb41d049b831f1a3ee0c3eada63e34557b30702b50ab59fb372feff7c30b8cbb7dd51afbe88444ec56238722ec1
+ d = 0xb07cf179604f83433186e5178760c759c15125ee54ff6f8dcde46e872b709ac82ed0bd0a4e036d774034dcb18a9fb11894657a1485895f80
+ point = EccPoint(Px, Py, curve="Ed448")
+
+ # Private key only
+ key = ECC.construct(curve="Ed448", seed=seed)
+ self.assertEqual(key.pointQ, point)
+ self.assertTrue(key.has_private())
+
+ # Public key only
+ key = ECC.construct(curve="Ed448", point_x=Px, point_y=Py)
+ self.assertEqual(key.pointQ, point)
+ self.assertFalse(key.has_private())
+
+ # Private and public key
+ key = ECC.construct(curve="Ed448", seed=seed, point_x=Px, point_y=Py)
+ self.assertEqual(key.pointQ, point)
+ self.assertTrue(key.has_private())
+
+ # Other names
+ key = ECC.construct(curve="ed448", seed=seed)
+
+ def test_negative_construct(self):
+ coord = dict(point_x=10, point_y=4)
+ coordG = dict(point_x=_curves['ed448'].Gx, point_y=_curves['ed448'].Gy)
+
+ self.assertRaises(ValueError, ECC.construct, curve="Ed448", **coord)
+ self.assertRaises(ValueError, ECC.construct, curve="Ed448", d=2, **coordG)
+ self.assertRaises(ValueError, ECC.construct, curve="Ed448", seed=b'H'*58)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TestEccPoint_Ed448)
+ tests += list_test_cases(TestEccKey_Ed448)
+ tests += list_test_cases(TestEccModule_Ed448)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_NIST.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_NIST.py
new file mode 100644
index 00000000..fa8dfe87
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ECC_NIST.py
@@ -0,0 +1,1440 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors
+
+from Crypto.PublicKey import ECC
+from Crypto.PublicKey.ECC import EccPoint, _curves, EccKey
+
+from Crypto.Math.Numbers import Integer
+
+
+class TestEccPoint(unittest.TestCase):
+
+ def test_mix(self):
+
+ p1 = ECC.generate(curve='P-256').pointQ
+ p2 = ECC.generate(curve='P-384').pointQ
+
+ try:
+ p1 + p2
+ assert(False)
+ except ValueError as e:
+ assert "not on the same curve" in str(e)
+
+ try:
+ p1 += p2
+ assert(False)
+ except ValueError as e:
+ assert "not on the same curve" in str(e)
+
+ class OtherKeyType:
+ pass
+
+ self.assertFalse(p1 == OtherKeyType())
+ self.assertTrue(p1 != OtherKeyType())
+
+ def test_repr(self):
+ p1 = ECC.construct(curve='P-256',
+ d=75467964919405407085864614198393977741148485328036093939970922195112333446269,
+ point_x=20573031766139722500939782666697015100983491952082159880539639074939225934381,
+ point_y=108863130203210779921520632367477406025152638284581252625277850513266505911389)
+ self.assertEqual(repr(p1), "EccKey(curve='NIST P-256', point_x=20573031766139722500939782666697015100983491952082159880539639074939225934381, point_y=108863130203210779921520632367477406025152638284581252625277850513266505911389, d=75467964919405407085864614198393977741148485328036093939970922195112333446269)")
+
+
+class TestEccPoint_NIST_P192(unittest.TestCase):
+ """Tests defined in section 4.1 of https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.204.9073&rep=rep1&type=pdf"""
+
+ pointS = EccPoint(
+ 0xd458e7d127ae671b0c330266d246769353a012073e97acf8,
+ 0x325930500d851f336bddc050cf7fb11b5673a1645086df3b,
+ curve='p192')
+
+ pointT = EccPoint(
+ 0xf22c4395213e9ebe67ddecdd87fdbd01be16fb059b9753a4,
+ 0x264424096af2b3597796db48f8dfb41fa9cecc97691a9c79,
+ curve='p192')
+
+ def test_curve_attribute(self):
+ self.assertEqual(self.pointS.curve, "NIST P-192")
+
+ def test_set(self):
+ pointW = EccPoint(0, 0)
+ pointW.set(self.pointS)
+ self.assertEqual(pointW, self.pointS)
+
+ def test_copy(self):
+ pointW = self.pointS.copy()
+ self.assertEqual(pointW, self.pointS)
+ pointW.set(self.pointT)
+ self.assertEqual(pointW, self.pointT)
+ self.assertNotEqual(self.pointS, self.pointT)
+
+ def test_negate(self):
+ negS = -self.pointS
+ sum = self.pointS + negS
+ self.assertEqual(sum, self.pointS.point_at_infinity())
+
+ def test_addition(self):
+ pointRx = 0x48e1e4096b9b8e5ca9d0f1f077b8abf58e843894de4d0290
+ pointRy = 0x408fa77c797cd7dbfb16aa48a3648d3d63c94117d7b6aa4b
+
+ pointR = self.pointS + self.pointT
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pai = pointR.point_at_infinity()
+
+ # S + 0
+ pointR = self.pointS + pai
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + S
+ pointR = pai + self.pointS
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + 0
+ pointR = pai + pai
+ self.assertEqual(pointR, pai)
+
+ def test_inplace_addition(self):
+ pointRx = 0x48e1e4096b9b8e5ca9d0f1f077b8abf58e843894de4d0290
+ pointRy = 0x408fa77c797cd7dbfb16aa48a3648d3d63c94117d7b6aa4b
+
+ pointR = self.pointS.copy()
+ pointR += self.pointT
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pai = pointR.point_at_infinity()
+
+ # S + 0
+ pointR = self.pointS.copy()
+ pointR += pai
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + S
+ pointR = pai.copy()
+ pointR += self.pointS
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + 0
+ pointR = pai.copy()
+ pointR += pai
+ self.assertEqual(pointR, pai)
+
+ def test_doubling(self):
+ pointRx = 0x30c5bc6b8c7da25354b373dc14dd8a0eba42d25a3f6e6962
+ pointRy = 0x0dde14bc4249a721c407aedbf011e2ddbbcb2968c9d889cf
+
+ pointR = self.pointS.copy()
+ pointR.double()
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ # 2*0
+ pai = self.pointS.point_at_infinity()
+ pointR = pai.copy()
+ pointR.double()
+ self.assertEqual(pointR, pai)
+
+ # S + S
+ pointR = self.pointS.copy()
+ pointR += pointR
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_scalar_multiply(self):
+ d = 0xa78a236d60baec0c5dd41b33a542463a8255391af64c74ee
+ pointRx = 0x1faee4205a4f669d2d0a8f25e3bcec9a62a6952965bf6d31
+ pointRy = 0x5ff2cdfa508a2581892367087c696f179e7a4d7e8260fb06
+
+ pointR = self.pointS * d
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ # 0*S
+ pai = self.pointS.point_at_infinity()
+ pointR = self.pointS * 0
+ self.assertEqual(pointR, pai)
+
+ # -1*S
+ self.assertRaises(ValueError, lambda: self.pointS * -1)
+
+ # Reverse order
+ pointR = d * self.pointS
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pointR = Integer(d) * self.pointS
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_joint_scalar_multiply(self):
+ d = 0xa78a236d60baec0c5dd41b33a542463a8255391af64c74ee
+ e = 0xc4be3d53ec3089e71e4de8ceab7cce889bc393cd85b972bc
+ pointRx = 0x019f64eed8fa9b72b7dfea82c17c9bfa60ecb9e1778b5bde
+ pointRy = 0x16590c5fcd8655fa4ced33fb800e2a7e3c61f35d83503644
+
+ pointR = self.pointS * d + self.pointT * e
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_sizes(self):
+ self.assertEqual(self.pointS.size_in_bits(), 192)
+ self.assertEqual(self.pointS.size_in_bytes(), 24)
+
+
+class TestEccPoint_NIST_P224(unittest.TestCase):
+ """Tests defined in section 4.2 of https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.204.9073&rep=rep1&type=pdf"""
+
+ pointS = EccPoint(
+ 0x6eca814ba59a930843dc814edd6c97da95518df3c6fdf16e9a10bb5b,
+ 0xef4b497f0963bc8b6aec0ca0f259b89cd80994147e05dc6b64d7bf22,
+ curve='p224')
+
+ pointT = EccPoint(
+ 0xb72b25aea5cb03fb88d7e842002969648e6ef23c5d39ac903826bd6d,
+ 0xc42a8a4d34984f0b71b5b4091af7dceb33ea729c1a2dc8b434f10c34,
+ curve='p224')
+
+ def test_curve_attribute(self):
+ self.assertEqual(self.pointS.curve, "NIST P-224")
+
+ def test_set(self):
+ pointW = EccPoint(0, 0)
+ pointW.set(self.pointS)
+ self.assertEqual(pointW, self.pointS)
+
+ def test_copy(self):
+ pointW = self.pointS.copy()
+ self.assertEqual(pointW, self.pointS)
+ pointW.set(self.pointT)
+ self.assertEqual(pointW, self.pointT)
+ self.assertNotEqual(self.pointS, self.pointT)
+
+ def test_negate(self):
+ negS = -self.pointS
+ sum = self.pointS + negS
+ self.assertEqual(sum, self.pointS.point_at_infinity())
+
+ def test_addition(self):
+ pointRx = 0x236f26d9e84c2f7d776b107bd478ee0a6d2bcfcaa2162afae8d2fd15
+ pointRy = 0xe53cc0a7904ce6c3746f6a97471297a0b7d5cdf8d536ae25bb0fda70
+
+ pointR = self.pointS + self.pointT
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pai = pointR.point_at_infinity()
+
+ # S + 0
+ pointR = self.pointS + pai
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + S
+ pointR = pai + self.pointS
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + 0
+ pointR = pai + pai
+ self.assertEqual(pointR, pai)
+
+ def test_inplace_addition(self):
+ pointRx = 0x236f26d9e84c2f7d776b107bd478ee0a6d2bcfcaa2162afae8d2fd15
+ pointRy = 0xe53cc0a7904ce6c3746f6a97471297a0b7d5cdf8d536ae25bb0fda70
+
+ pointR = self.pointS.copy()
+ pointR += self.pointT
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pai = pointR.point_at_infinity()
+
+ # S + 0
+ pointR = self.pointS.copy()
+ pointR += pai
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + S
+ pointR = pai.copy()
+ pointR += self.pointS
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + 0
+ pointR = pai.copy()
+ pointR += pai
+ self.assertEqual(pointR, pai)
+
+ def test_doubling(self):
+ pointRx = 0xa9c96f2117dee0f27ca56850ebb46efad8ee26852f165e29cb5cdfc7
+ pointRy = 0xadf18c84cf77ced4d76d4930417d9579207840bf49bfbf5837dfdd7d
+
+ pointR = self.pointS.copy()
+ pointR.double()
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ # 2*0
+ pai = self.pointS.point_at_infinity()
+ pointR = pai.copy()
+ pointR.double()
+ self.assertEqual(pointR, pai)
+
+ # S + S
+ pointR = self.pointS.copy()
+ pointR += pointR
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_scalar_multiply(self):
+ d = 0xa78ccc30eaca0fcc8e36b2dd6fbb03df06d37f52711e6363aaf1d73b
+ pointRx = 0x96a7625e92a8d72bff1113abdb95777e736a14c6fdaacc392702bca4
+ pointRy = 0x0f8e5702942a3c5e13cd2fd5801915258b43dfadc70d15dbada3ed10
+
+ pointR = self.pointS * d
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ # 0*S
+ pai = self.pointS.point_at_infinity()
+ pointR = self.pointS * 0
+ self.assertEqual(pointR, pai)
+
+ # -1*S
+ self.assertRaises(ValueError, lambda: self.pointS * -1)
+
+ # Reverse order
+ pointR = d * self.pointS
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pointR = Integer(d) * self.pointS
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_joing_scalar_multiply(self):
+ d = 0xa78ccc30eaca0fcc8e36b2dd6fbb03df06d37f52711e6363aaf1d73b
+ e = 0x54d549ffc08c96592519d73e71e8e0703fc8177fa88aa77a6ed35736
+ pointRx = 0xdbfe2958c7b2cda1302a67ea3ffd94c918c5b350ab838d52e288c83e
+ pointRy = 0x2f521b83ac3b0549ff4895abcc7f0c5a861aacb87acbc5b8147bb18b
+
+ pointR = self.pointS * d + self.pointT * e
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_sizes(self):
+ self.assertEqual(self.pointS.size_in_bits(), 224)
+ self.assertEqual(self.pointS.size_in_bytes(), 28)
+
+
+class TestEccPoint_NIST_P256(unittest.TestCase):
+ """Tests defined in section 4.3 of https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.204.9073&rep=rep1&type=pdf"""
+
+ pointS = EccPoint(
+ 0xde2444bebc8d36e682edd27e0f271508617519b3221a8fa0b77cab3989da97c9,
+ 0xc093ae7ff36e5380fc01a5aad1e66659702de80f53cec576b6350b243042a256)
+
+ pointT = EccPoint(
+ 0x55a8b00f8da1d44e62f6b3b25316212e39540dc861c89575bb8cf92e35e0986b,
+ 0x5421c3209c2d6c704835d82ac4c3dd90f61a8a52598b9e7ab656e9d8c8b24316)
+
+ def test_curve_attribute(self):
+ self.assertEqual(self.pointS.curve, "NIST P-256")
+
+ def test_set(self):
+ pointW = EccPoint(0, 0)
+ pointW.set(self.pointS)
+ self.assertEqual(pointW, self.pointS)
+
+ def test_copy(self):
+ pointW = self.pointS.copy()
+ self.assertEqual(pointW, self.pointS)
+ pointW.set(self.pointT)
+ self.assertEqual(pointW, self.pointT)
+ self.assertNotEqual(self.pointS, self.pointT)
+
+ def test_negate(self):
+ negS = -self.pointS
+ sum = self.pointS + negS
+ self.assertEqual(sum, self.pointS.point_at_infinity())
+
+ def test_addition(self):
+ pointRx = 0x72b13dd4354b6b81745195e98cc5ba6970349191ac476bd4553cf35a545a067e
+ pointRy = 0x8d585cbb2e1327d75241a8a122d7620dc33b13315aa5c9d46d013011744ac264
+
+ pointR = self.pointS + self.pointT
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pai = pointR.point_at_infinity()
+
+ # S + 0
+ pointR = self.pointS + pai
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + S
+ pointR = pai + self.pointS
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + 0
+ pointR = pai + pai
+ self.assertEqual(pointR, pai)
+
+ def test_inplace_addition(self):
+ pointRx = 0x72b13dd4354b6b81745195e98cc5ba6970349191ac476bd4553cf35a545a067e
+ pointRy = 0x8d585cbb2e1327d75241a8a122d7620dc33b13315aa5c9d46d013011744ac264
+
+ pointR = self.pointS.copy()
+ pointR += self.pointT
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pai = pointR.point_at_infinity()
+
+ # S + 0
+ pointR = self.pointS.copy()
+ pointR += pai
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + S
+ pointR = pai.copy()
+ pointR += self.pointS
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + 0
+ pointR = pai.copy()
+ pointR += pai
+ self.assertEqual(pointR, pai)
+
+ def test_doubling(self):
+ pointRx = 0x7669e6901606ee3ba1a8eef1e0024c33df6c22f3b17481b82a860ffcdb6127b0
+ pointRy = 0xfa878162187a54f6c39f6ee0072f33de389ef3eecd03023de10ca2c1db61d0c7
+
+ pointR = self.pointS.copy()
+ pointR.double()
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ # 2*0
+ pai = self.pointS.point_at_infinity()
+ pointR = pai.copy()
+ pointR.double()
+ self.assertEqual(pointR, pai)
+
+ # S + S
+ pointR = self.pointS.copy()
+ pointR += pointR
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_scalar_multiply(self):
+ d = 0xc51e4753afdec1e6b6c6a5b992f43f8dd0c7a8933072708b6522468b2ffb06fd
+ pointRx = 0x51d08d5f2d4278882946d88d83c97d11e62becc3cfc18bedacc89ba34eeca03f
+ pointRy = 0x75ee68eb8bf626aa5b673ab51f6e744e06f8fcf8a6c0cf3035beca956a7b41d5
+
+ pointR = self.pointS * d
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ # 0*S
+ pai = self.pointS.point_at_infinity()
+ pointR = self.pointS * 0
+ self.assertEqual(pointR, pai)
+
+ # -1*S
+ self.assertRaises(ValueError, lambda: self.pointS * -1)
+
+ # Reverse order
+ pointR = d * self.pointS
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pointR = Integer(d) * self.pointS
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_joing_scalar_multiply(self):
+ d = 0xc51e4753afdec1e6b6c6a5b992f43f8dd0c7a8933072708b6522468b2ffb06fd
+ e = 0xd37f628ece72a462f0145cbefe3f0b355ee8332d37acdd83a358016aea029db7
+ pointRx = 0xd867b4679221009234939221b8046245efcf58413daacbeff857b8588341f6b8
+ pointRy = 0xf2504055c03cede12d22720dad69c745106b6607ec7e50dd35d54bd80f615275
+
+ pointR = self.pointS * d + self.pointT * e
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_sizes(self):
+ self.assertEqual(self.pointS.size_in_bits(), 256)
+ self.assertEqual(self.pointS.size_in_bytes(), 32)
+
+
+class TestEccPoint_NIST_P384(unittest.TestCase):
+ """Tests defined in section 4.4 of https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.204.9073&rep=rep1&type=pdf"""
+
+ pointS = EccPoint(
+ 0xfba203b81bbd23f2b3be971cc23997e1ae4d89e69cb6f92385dda82768ada415ebab4167459da98e62b1332d1e73cb0e,
+ 0x5ffedbaefdeba603e7923e06cdb5d0c65b22301429293376d5c6944e3fa6259f162b4788de6987fd59aed5e4b5285e45,
+ "p384")
+
+ pointT = EccPoint(
+ 0xaacc05202e7fda6fc73d82f0a66220527da8117ee8f8330ead7d20ee6f255f582d8bd38c5a7f2b40bcdb68ba13d81051,
+ 0x84009a263fefba7c2c57cffa5db3634d286131afc0fca8d25afa22a7b5dce0d9470da89233cee178592f49b6fecb5092,
+ "p384")
+
+ def test_curve_attribute(self):
+ self.assertEqual(self.pointS.curve, "NIST P-384")
+
+ def test_set(self):
+ pointW = EccPoint(0, 0, "p384")
+ pointW.set(self.pointS)
+ self.assertEqual(pointW, self.pointS)
+
+ def test_copy(self):
+ pointW = self.pointS.copy()
+ self.assertEqual(pointW, self.pointS)
+ pointW.set(self.pointT)
+ self.assertEqual(pointW, self.pointT)
+ self.assertNotEqual(self.pointS, self.pointT)
+
+ def test_negate(self):
+ negS = -self.pointS
+ sum = self.pointS + negS
+ self.assertEqual(sum, self.pointS.point_at_infinity())
+
+ def test_addition(self):
+ pointRx = 0x12dc5ce7acdfc5844d939f40b4df012e68f865b89c3213ba97090a247a2fc009075cf471cd2e85c489979b65ee0b5eed
+ pointRy = 0x167312e58fe0c0afa248f2854e3cddcb557f983b3189b67f21eee01341e7e9fe67f6ee81b36988efa406945c8804a4b0
+
+ pointR = self.pointS + self.pointT
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pai = pointR.point_at_infinity()
+
+ # S + 0
+ pointR = self.pointS + pai
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + S
+ pointR = pai + self.pointS
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + 0
+ pointR = pai + pai
+ self.assertEqual(pointR, pai)
+
+ def _test_inplace_addition(self):
+ pointRx = 0x72b13dd4354b6b81745195e98cc5ba6970349191ac476bd4553cf35a545a067e
+ pointRy = 0x8d585cbb2e1327d75241a8a122d7620dc33b13315aa5c9d46d013011744ac264
+
+ pointR = self.pointS.copy()
+ pointR += self.pointT
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pai = pointR.point_at_infinity()
+
+ # S + 0
+ pointR = self.pointS.copy()
+ pointR += pai
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + S
+ pointR = pai.copy()
+ pointR += self.pointS
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + 0
+ pointR = pai.copy()
+ pointR += pai
+ self.assertEqual(pointR, pai)
+
+ def test_doubling(self):
+ pointRx = 0x2a2111b1e0aa8b2fc5a1975516bc4d58017ff96b25e1bdff3c229d5fac3bacc319dcbec29f9478f42dee597b4641504c
+ pointRy = 0xfa2e3d9dc84db8954ce8085ef28d7184fddfd1344b4d4797343af9b5f9d837520b450f726443e4114bd4e5bdb2f65ddd
+
+ pointR = self.pointS.copy()
+ pointR.double()
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ # 2*0
+ pai = self.pointS.point_at_infinity()
+ pointR = pai.copy()
+ pointR.double()
+ self.assertEqual(pointR, pai)
+
+ # S + S
+ pointR = self.pointS.copy()
+ pointR += pointR
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_scalar_multiply(self):
+ d = 0xa4ebcae5a665983493ab3e626085a24c104311a761b5a8fdac052ed1f111a5c44f76f45659d2d111a61b5fdd97583480
+ pointRx = 0xe4f77e7ffeb7f0958910e3a680d677a477191df166160ff7ef6bb5261f791aa7b45e3e653d151b95dad3d93ca0290ef2
+ pointRy = 0xac7dee41d8c5f4a7d5836960a773cfc1376289d3373f8cf7417b0c6207ac32e913856612fc9ff2e357eb2ee05cf9667f
+
+ pointR = self.pointS * d
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ # 0*S
+ pai = self.pointS.point_at_infinity()
+ pointR = self.pointS * 0
+ self.assertEqual(pointR, pai)
+
+ # -1*S
+ self.assertRaises(ValueError, lambda: self.pointS * -1)
+
+ def test_joing_scalar_multiply(self):
+ d = 0xa4ebcae5a665983493ab3e626085a24c104311a761b5a8fdac052ed1f111a5c44f76f45659d2d111a61b5fdd97583480
+ e = 0xafcf88119a3a76c87acbd6008e1349b29f4ba9aa0e12ce89bcfcae2180b38d81ab8cf15095301a182afbc6893e75385d
+ pointRx = 0x917ea28bcd641741ae5d18c2f1bd917ba68d34f0f0577387dc81260462aea60e2417b8bdc5d954fc729d211db23a02dc
+ pointRy = 0x1a29f7ce6d074654d77b40888c73e92546c8f16a5ff6bcbd307f758d4aee684beff26f6742f597e2585c86da908f7186
+
+ pointR = self.pointS * d + self.pointT * e
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_sizes(self):
+ self.assertEqual(self.pointS.size_in_bits(), 384)
+ self.assertEqual(self.pointS.size_in_bytes(), 48)
+
+
+class TestEccPoint_NIST_P521(unittest.TestCase):
+ """Tests defined in section 4.5 of https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.204.9073&rep=rep1&type=pdf"""
+
+ pointS = EccPoint(
+ 0x000001d5c693f66c08ed03ad0f031f937443458f601fd098d3d0227b4bf62873af50740b0bb84aa157fc847bcf8dc16a8b2b8bfd8e2d0a7d39af04b089930ef6dad5c1b4,
+ 0x00000144b7770963c63a39248865ff36b074151eac33549b224af5c8664c54012b818ed037b2b7c1a63ac89ebaa11e07db89fcee5b556e49764ee3fa66ea7ae61ac01823,
+ "p521")
+
+ pointT = EccPoint(
+ 0x000000f411f2ac2eb971a267b80297ba67c322dba4bb21cec8b70073bf88fc1ca5fde3ba09e5df6d39acb2c0762c03d7bc224a3e197feaf760d6324006fe3be9a548c7d5,
+ 0x000001fdf842769c707c93c630df6d02eff399a06f1b36fb9684f0b373ed064889629abb92b1ae328fdb45534268384943f0e9222afe03259b32274d35d1b9584c65e305,
+ "p521")
+
+ def test_curve_attribute(self):
+ self.assertEqual(self.pointS.curve, "NIST P-521")
+
+ def test_set(self):
+ pointW = EccPoint(0, 0)
+ pointW.set(self.pointS)
+ self.assertEqual(pointW, self.pointS)
+
+ def test_copy(self):
+ pointW = self.pointS.copy()
+ self.assertEqual(pointW, self.pointS)
+ pointW.set(self.pointT)
+ self.assertEqual(pointW, self.pointT)
+ self.assertNotEqual(self.pointS, self.pointT)
+
+ def test_negate(self):
+ negS = -self.pointS
+ sum = self.pointS + negS
+ self.assertEqual(sum, self.pointS.point_at_infinity())
+
+ def test_addition(self):
+ pointRx = 0x000001264ae115ba9cbc2ee56e6f0059e24b52c8046321602c59a339cfb757c89a59c358a9a8e1f86d384b3f3b255ea3f73670c6dc9f45d46b6a196dc37bbe0f6b2dd9e9
+ pointRy = 0x00000062a9c72b8f9f88a271690bfa017a6466c31b9cadc2fc544744aeb817072349cfddc5ad0e81b03f1897bd9c8c6efbdf68237dc3bb00445979fb373b20c9a967ac55
+
+ pointR = self.pointS + self.pointT
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pai = pointR.point_at_infinity()
+
+ # S + 0
+ pointR = self.pointS + pai
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + S
+ pointR = pai + self.pointS
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + 0
+ pointR = pai + pai
+ self.assertEqual(pointR, pai)
+
+ def test_inplace_addition(self):
+ pointRx = 0x000001264ae115ba9cbc2ee56e6f0059e24b52c8046321602c59a339cfb757c89a59c358a9a8e1f86d384b3f3b255ea3f73670c6dc9f45d46b6a196dc37bbe0f6b2dd9e9
+ pointRy = 0x00000062a9c72b8f9f88a271690bfa017a6466c31b9cadc2fc544744aeb817072349cfddc5ad0e81b03f1897bd9c8c6efbdf68237dc3bb00445979fb373b20c9a967ac55
+
+ pointR = self.pointS.copy()
+ pointR += self.pointT
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ pai = pointR.point_at_infinity()
+
+ # S + 0
+ pointR = self.pointS.copy()
+ pointR += pai
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + S
+ pointR = pai.copy()
+ pointR += self.pointS
+ self.assertEqual(pointR, self.pointS)
+
+ # 0 + 0
+ pointR = pai.copy()
+ pointR += pai
+ self.assertEqual(pointR, pai)
+
+ def test_doubling(self):
+ pointRx = 0x0000012879442f2450c119e7119a5f738be1f1eba9e9d7c6cf41b325d9ce6d643106e9d61124a91a96bcf201305a9dee55fa79136dc700831e54c3ca4ff2646bd3c36bc6
+ pointRy = 0x0000019864a8b8855c2479cbefe375ae553e2393271ed36fadfc4494fc0583f6bd03598896f39854abeae5f9a6515a021e2c0eef139e71de610143f53382f4104dccb543
+
+ pointR = self.pointS.copy()
+ pointR.double()
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ # 2*0
+ pai = self.pointS.point_at_infinity()
+ pointR = pai.copy()
+ pointR.double()
+ self.assertEqual(pointR, pai)
+
+ # S + S
+ pointR = self.pointS.copy()
+ pointR += pointR
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_scalar_multiply(self):
+ d = 0x000001eb7f81785c9629f136a7e8f8c674957109735554111a2a866fa5a166699419bfa9936c78b62653964df0d6da940a695c7294d41b2d6600de6dfcf0edcfc89fdcb1
+ pointRx = 0x00000091b15d09d0ca0353f8f96b93cdb13497b0a4bb582ae9ebefa35eee61bf7b7d041b8ec34c6c00c0c0671c4ae063318fb75be87af4fe859608c95f0ab4774f8c95bb
+ pointRy = 0x00000130f8f8b5e1abb4dd94f6baaf654a2d5810411e77b7423965e0c7fd79ec1ae563c207bd255ee9828eb7a03fed565240d2cc80ddd2cecbb2eb50f0951f75ad87977f
+
+ pointR = self.pointS * d
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ # 0*S
+ pai = self.pointS.point_at_infinity()
+ pointR = self.pointS * 0
+ self.assertEqual(pointR, pai)
+
+ # -1*S
+ self.assertRaises(ValueError, lambda: self.pointS * -1)
+
+ def test_joing_scalar_multiply(self):
+ d = 0x000001eb7f81785c9629f136a7e8f8c674957109735554111a2a866fa5a166699419bfa9936c78b62653964df0d6da940a695c7294d41b2d6600de6dfcf0edcfc89fdcb1
+ e = 0x00000137e6b73d38f153c3a7575615812608f2bab3229c92e21c0d1c83cfad9261dbb17bb77a63682000031b9122c2f0cdab2af72314be95254de4291a8f85f7c70412e3
+ pointRx = 0x0000009d3802642b3bea152beb9e05fba247790f7fc168072d363340133402f2585588dc1385d40ebcb8552f8db02b23d687cae46185b27528adb1bf9729716e4eba653d
+ pointRy = 0x0000000fe44344e79da6f49d87c1063744e5957d9ac0a505bafa8281c9ce9ff25ad53f8da084a2deb0923e46501de5797850c61b229023dd9cf7fc7f04cd35ebb026d89d
+
+ pointR = self.pointS * d
+ pointR += self.pointT * e
+ self.assertEqual(pointR.x, pointRx)
+ self.assertEqual(pointR.y, pointRy)
+
+ def test_sizes(self):
+ self.assertEqual(self.pointS.size_in_bits(), 521)
+ self.assertEqual(self.pointS.size_in_bytes(), 66)
+
+
+class TestEccPoint_PAI_P192(unittest.TestCase):
+ """Test vectors from http://point-at-infinity.org/ecc/nisttv"""
+
+ curve = _curves['p192']
+ pointG = EccPoint(curve.Gx, curve.Gy, "p192")
+
+
+tv_pai = load_test_vectors(("PublicKey", "ECC"),
+ "point-at-infinity.org-P192.txt",
+ "P-192 tests from point-at-infinity.org",
+ {"k": lambda k: int(k),
+ "x": lambda x: int(x, 16),
+ "y": lambda y: int(y, 16)}) or []
+for tv in tv_pai:
+ def new_test(self, scalar=tv.k, x=tv.x, y=tv.y):
+ result = self.pointG * scalar
+ self.assertEqual(result.x, x)
+ self.assertEqual(result.y, y)
+ setattr(TestEccPoint_PAI_P192, "test_%d" % tv.count, new_test)
+
+
+class TestEccPoint_PAI_P224(unittest.TestCase):
+ """Test vectors from http://point-at-infinity.org/ecc/nisttv"""
+
+ curve = _curves['p224']
+ pointG = EccPoint(curve.Gx, curve.Gy, "p224")
+
+
+tv_pai = load_test_vectors(("PublicKey", "ECC"),
+ "point-at-infinity.org-P224.txt",
+ "P-224 tests from point-at-infinity.org",
+ {"k": lambda k: int(k),
+ "x": lambda x: int(x, 16),
+ "y": lambda y: int(y, 16)}) or []
+for tv in tv_pai:
+ def new_test(self, scalar=tv.k, x=tv.x, y=tv.y):
+ result = self.pointG * scalar
+ self.assertEqual(result.x, x)
+ self.assertEqual(result.y, y)
+ setattr(TestEccPoint_PAI_P224, "test_%d" % tv.count, new_test)
+
+
+class TestEccPoint_PAI_P256(unittest.TestCase):
+ """Test vectors from http://point-at-infinity.org/ecc/nisttv"""
+
+ curve = _curves['p256']
+ pointG = EccPoint(curve.Gx, curve.Gy, "p256")
+
+
+tv_pai = load_test_vectors(("PublicKey", "ECC"),
+ "point-at-infinity.org-P256.txt",
+ "P-256 tests from point-at-infinity.org",
+ {"k": lambda k: int(k),
+ "x": lambda x: int(x, 16),
+ "y": lambda y: int(y, 16)}) or []
+for tv in tv_pai:
+ def new_test(self, scalar=tv.k, x=tv.x, y=tv.y):
+ result = self.pointG * scalar
+ self.assertEqual(result.x, x)
+ self.assertEqual(result.y, y)
+ setattr(TestEccPoint_PAI_P256, "test_%d" % tv.count, new_test)
+
+
+class TestEccPoint_PAI_P384(unittest.TestCase):
+ """Test vectors from http://point-at-infinity.org/ecc/nisttv"""
+
+ curve = _curves['p384']
+ pointG = EccPoint(curve.Gx, curve.Gy, "p384")
+
+
+tv_pai = load_test_vectors(("PublicKey", "ECC"),
+ "point-at-infinity.org-P384.txt",
+ "P-384 tests from point-at-infinity.org",
+ {"k": lambda k: int(k),
+ "x": lambda x: int(x, 16),
+ "y": lambda y: int(y, 16)}) or []
+for tv in tv_pai:
+ def new_test(self, scalar=tv.k, x=tv.x, y=tv.y):
+ result = self.pointG * scalar
+ self.assertEqual(result.x, x)
+ self.assertEqual(result.y, y)
+ setattr(TestEccPoint_PAI_P384, "test_%d" % tv.count, new_test)
+
+
+class TestEccPoint_PAI_P521(unittest.TestCase):
+ """Test vectors from http://point-at-infinity.org/ecc/nisttv"""
+
+ curve = _curves['p521']
+ pointG = EccPoint(curve.Gx, curve.Gy, "p521")
+
+
+tv_pai = load_test_vectors(("PublicKey", "ECC"),
+ "point-at-infinity.org-P521.txt",
+ "P-521 tests from point-at-infinity.org",
+ {"k": lambda k: int(k),
+ "x": lambda x: int(x, 16),
+ "y": lambda y: int(y, 16)}) or []
+for tv in tv_pai:
+ def new_test(self, scalar=tv.k, x=tv.x, y=tv.y):
+ result = self.pointG * scalar
+ self.assertEqual(result.x, x)
+ self.assertEqual(result.y, y)
+ setattr(TestEccPoint_PAI_P521, "test_%d" % tv.count, new_test)
+
+
+class TestEccKey_P192(unittest.TestCase):
+
+ def test_private_key(self):
+
+ key = EccKey(curve="P-192", d=1)
+ self.assertEqual(key.d, 1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, _curves['p192'].Gx)
+ self.assertEqual(key.pointQ.y, _curves['p192'].Gy)
+
+ point = EccPoint(_curves['p192'].Gx, _curves['p192'].Gy, curve='P-192')
+ key = EccKey(curve="P-192", d=1, point=point)
+ self.assertEqual(key.d, 1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ # Other names
+ key = EccKey(curve="secp192r1", d=1)
+ key = EccKey(curve="prime192v1", d=1)
+
+ def test_public_key(self):
+
+ point = EccPoint(_curves['p192'].Gx, _curves['p192'].Gy, curve='P-192')
+ key = EccKey(curve="P-192", point=point)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ def test_public_key_derived(self):
+
+ priv_key = EccKey(curve="P-192", d=3)
+ pub_key = priv_key.public_key()
+ self.assertFalse(pub_key.has_private())
+ self.assertEqual(priv_key.pointQ, pub_key.pointQ)
+
+ def test_invalid_curve(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-193", d=1))
+
+ def test_invalid_d(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-192", d=0))
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-192",
+ d=_curves['p192'].order))
+
+ def test_equality(self):
+
+ private_key = ECC.construct(d=3, curve="P-192")
+ private_key2 = ECC.construct(d=3, curve="P-192")
+ private_key3 = ECC.construct(d=4, curve="P-192")
+
+ public_key = private_key.public_key()
+ public_key2 = private_key2.public_key()
+ public_key3 = private_key3.public_key()
+
+ self.assertEqual(private_key, private_key2)
+ self.assertNotEqual(private_key, private_key3)
+
+ self.assertEqual(public_key, public_key2)
+ self.assertNotEqual(public_key, public_key3)
+
+ self.assertNotEqual(public_key, private_key)
+
+ def test_name_consistency(self):
+ key = ECC.generate(curve='p192')
+ self.assertIn("curve='NIST P-192'", repr(key))
+ self.assertEqual(key.curve, 'NIST P-192')
+ self.assertEqual(key.public_key().curve, 'NIST P-192')
+
+
+class TestEccKey_P224(unittest.TestCase):
+
+ def test_private_key(self):
+
+ key = EccKey(curve="P-224", d=1)
+ self.assertEqual(key.d, 1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, _curves['p224'].Gx)
+ self.assertEqual(key.pointQ.y, _curves['p224'].Gy)
+
+ point = EccPoint(_curves['p224'].Gx, _curves['p224'].Gy, curve='P-224')
+ key = EccKey(curve="P-224", d=1, point=point)
+ self.assertEqual(key.d, 1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ # Other names
+ key = EccKey(curve="secp224r1", d=1)
+ key = EccKey(curve="prime224v1", d=1)
+
+ def test_public_key(self):
+
+ point = EccPoint(_curves['p224'].Gx, _curves['p224'].Gy, curve='P-224')
+ key = EccKey(curve="P-224", point=point)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ def test_public_key_derived(self):
+
+ priv_key = EccKey(curve="P-224", d=3)
+ pub_key = priv_key.public_key()
+ self.assertFalse(pub_key.has_private())
+ self.assertEqual(priv_key.pointQ, pub_key.pointQ)
+
+ def test_invalid_curve(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-225", d=1))
+
+ def test_invalid_d(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-224", d=0))
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-224",
+ d=_curves['p224'].order))
+
+ def test_equality(self):
+
+ private_key = ECC.construct(d=3, curve="P-224")
+ private_key2 = ECC.construct(d=3, curve="P-224")
+ private_key3 = ECC.construct(d=4, curve="P-224")
+
+ public_key = private_key.public_key()
+ public_key2 = private_key2.public_key()
+ public_key3 = private_key3.public_key()
+
+ self.assertEqual(private_key, private_key2)
+ self.assertNotEqual(private_key, private_key3)
+
+ self.assertEqual(public_key, public_key2)
+ self.assertNotEqual(public_key, public_key3)
+
+ self.assertNotEqual(public_key, private_key)
+
+ def test_name_consistency(self):
+ key = ECC.generate(curve='p224')
+ self.assertIn("curve='NIST P-224'", repr(key))
+ self.assertEqual(key.curve, 'NIST P-224')
+ self.assertEqual(key.public_key().curve, 'NIST P-224')
+
+
+class TestEccKey_P256(unittest.TestCase):
+
+ def test_private_key(self):
+
+ key = EccKey(curve="P-256", d=1)
+ self.assertEqual(key.d, 1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, _curves['p256'].Gx)
+ self.assertEqual(key.pointQ.y, _curves['p256'].Gy)
+
+ point = EccPoint(_curves['p256'].Gx, _curves['p256'].Gy)
+ key = EccKey(curve="P-256", d=1, point=point)
+ self.assertEqual(key.d, 1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ # Other names
+ key = EccKey(curve="secp256r1", d=1)
+ key = EccKey(curve="prime256v1", d=1)
+
+ # Must not accept d parameter
+ self.assertRaises(ValueError, EccKey, curve="p256", seed=b'H'*32)
+
+ def test_public_key(self):
+
+ point = EccPoint(_curves['p256'].Gx, _curves['p256'].Gy)
+ key = EccKey(curve="P-256", point=point)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ def test_public_key_derived(self):
+
+ priv_key = EccKey(curve="P-256", d=3)
+ pub_key = priv_key.public_key()
+ self.assertFalse(pub_key.has_private())
+ self.assertEqual(priv_key.pointQ, pub_key.pointQ)
+
+ def test_invalid_curve(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-257", d=1))
+
+ def test_invalid_d(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-256", d=0))
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-256", d=_curves['p256'].order))
+
+ def test_equality(self):
+
+ private_key = ECC.construct(d=3, curve="P-256")
+ private_key2 = ECC.construct(d=3, curve="P-256")
+ private_key3 = ECC.construct(d=4, curve="P-256")
+
+ public_key = private_key.public_key()
+ public_key2 = private_key2.public_key()
+ public_key3 = private_key3.public_key()
+
+ self.assertEqual(private_key, private_key2)
+ self.assertNotEqual(private_key, private_key3)
+
+ self.assertEqual(public_key, public_key2)
+ self.assertNotEqual(public_key, public_key3)
+
+ self.assertNotEqual(public_key, private_key)
+
+ def test_name_consistency(self):
+ key = ECC.generate(curve='p256')
+ self.assertIn("curve='NIST P-256'", repr(key))
+ self.assertEqual(key.curve, 'NIST P-256')
+ self.assertEqual(key.public_key().curve, 'NIST P-256')
+
+
+class TestEccKey_P384(unittest.TestCase):
+
+ def test_private_key(self):
+
+ p384 = _curves['p384']
+
+ key = EccKey(curve="P-384", d=1)
+ self.assertEqual(key.d, 1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, p384.Gx)
+ self.assertEqual(key.pointQ.y, p384.Gy)
+
+ point = EccPoint(p384.Gx, p384.Gy, "p384")
+ key = EccKey(curve="P-384", d=1, point=point)
+ self.assertEqual(key.d, 1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ # Other names
+ key = EccKey(curve="p384", d=1)
+ key = EccKey(curve="secp384r1", d=1)
+ key = EccKey(curve="prime384v1", d=1)
+
+ def test_public_key(self):
+
+ p384 = _curves['p384']
+ point = EccPoint(p384.Gx, p384.Gy, 'p384')
+ key = EccKey(curve="P-384", point=point)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ def test_public_key_derived(self):
+
+ priv_key = EccKey(curve="P-384", d=3)
+ pub_key = priv_key.public_key()
+ self.assertFalse(pub_key.has_private())
+ self.assertEqual(priv_key.pointQ, pub_key.pointQ)
+
+ def test_invalid_curve(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-385", d=1))
+
+ def test_invalid_d(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-384", d=0))
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-384",
+ d=_curves['p384'].order))
+
+ def test_equality(self):
+
+ private_key = ECC.construct(d=3, curve="P-384")
+ private_key2 = ECC.construct(d=3, curve="P-384")
+ private_key3 = ECC.construct(d=4, curve="P-384")
+
+ public_key = private_key.public_key()
+ public_key2 = private_key2.public_key()
+ public_key3 = private_key3.public_key()
+
+ self.assertEqual(private_key, private_key2)
+ self.assertNotEqual(private_key, private_key3)
+
+ self.assertEqual(public_key, public_key2)
+ self.assertNotEqual(public_key, public_key3)
+
+ self.assertNotEqual(public_key, private_key)
+
+ def test_name_consistency(self):
+ key = ECC.generate(curve='p384')
+ self.assertIn("curve='NIST P-384'", repr(key))
+ self.assertEqual(key.curve, 'NIST P-384')
+ self.assertEqual(key.public_key().curve, 'NIST P-384')
+
+
+class TestEccKey_P521(unittest.TestCase):
+
+ def test_private_key(self):
+
+ p521 = _curves['p521']
+
+ key = EccKey(curve="P-521", d=1)
+ self.assertEqual(key.d, 1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ.x, p521.Gx)
+ self.assertEqual(key.pointQ.y, p521.Gy)
+
+ point = EccPoint(p521.Gx, p521.Gy, "p521")
+ key = EccKey(curve="P-521", d=1, point=point)
+ self.assertEqual(key.d, 1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ # Other names
+ key = EccKey(curve="p521", d=1)
+ key = EccKey(curve="secp521r1", d=1)
+ key = EccKey(curve="prime521v1", d=1)
+
+ def test_public_key(self):
+
+ p521 = _curves['p521']
+ point = EccPoint(p521.Gx, p521.Gy, 'p521')
+ key = EccKey(curve="P-384", point=point)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, point)
+
+ def test_public_key_derived(self):
+
+ priv_key = EccKey(curve="P-521", d=3)
+ pub_key = priv_key.public_key()
+ self.assertFalse(pub_key.has_private())
+ self.assertEqual(priv_key.pointQ, pub_key.pointQ)
+
+ def test_invalid_curve(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-522", d=1))
+
+ def test_invalid_d(self):
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-521", d=0))
+ self.assertRaises(ValueError, lambda: EccKey(curve="P-521",
+ d=_curves['p521'].order))
+
+ def test_equality(self):
+
+ private_key = ECC.construct(d=3, curve="P-521")
+ private_key2 = ECC.construct(d=3, curve="P-521")
+ private_key3 = ECC.construct(d=4, curve="P-521")
+
+ public_key = private_key.public_key()
+ public_key2 = private_key2.public_key()
+ public_key3 = private_key3.public_key()
+
+ self.assertEqual(private_key, private_key2)
+ self.assertNotEqual(private_key, private_key3)
+
+ self.assertEqual(public_key, public_key2)
+ self.assertNotEqual(public_key, public_key3)
+
+ self.assertNotEqual(public_key, private_key)
+
+ def test_name_consistency(self):
+ key = ECC.generate(curve='p521')
+ self.assertIn("curve='NIST P-521'", repr(key))
+ self.assertEqual(key.curve, 'NIST P-521')
+ self.assertEqual(key.public_key().curve, 'NIST P-521')
+
+
+class TestEccModule_P192(unittest.TestCase):
+
+ def test_generate(self):
+
+ key = ECC.generate(curve="P-192")
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, EccPoint(_curves['p192'].Gx,
+ _curves['p192'].Gy,
+ "P-192") * key.d,
+ "p192")
+
+ # Other names
+ ECC.generate(curve="secp192r1")
+ ECC.generate(curve="prime192v1")
+
+ def test_construct(self):
+
+ key = ECC.construct(curve="P-192", d=1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, _curves['p192'].G)
+
+ key = ECC.construct(curve="P-192", point_x=_curves['p192'].Gx,
+ point_y=_curves['p192'].Gy)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, _curves['p192'].G)
+
+ # Other names
+ ECC.construct(curve="p192", d=1)
+ ECC.construct(curve="secp192r1", d=1)
+ ECC.construct(curve="prime192v1", d=1)
+
+ def test_negative_construct(self):
+ coord = dict(point_x=10, point_y=4)
+ coordG = dict(point_x=_curves['p192'].Gx, point_y=_curves['p192'].Gy)
+
+ self.assertRaises(ValueError, ECC.construct, curve="P-192", **coord)
+ self.assertRaises(ValueError, ECC.construct, curve="P-192", d=2, **coordG)
+
+
+class TestEccModule_P224(unittest.TestCase):
+
+ def test_generate(self):
+
+ key = ECC.generate(curve="P-224")
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, EccPoint(_curves['p224'].Gx,
+ _curves['p224'].Gy,
+ "P-224") * key.d,
+ "p224")
+
+ # Other names
+ ECC.generate(curve="secp224r1")
+ ECC.generate(curve="prime224v1")
+
+ def test_construct(self):
+
+ key = ECC.construct(curve="P-224", d=1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, _curves['p224'].G)
+
+ key = ECC.construct(curve="P-224", point_x=_curves['p224'].Gx,
+ point_y=_curves['p224'].Gy)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, _curves['p224'].G)
+
+ # Other names
+ ECC.construct(curve="p224", d=1)
+ ECC.construct(curve="secp224r1", d=1)
+ ECC.construct(curve="prime224v1", d=1)
+
+ def test_negative_construct(self):
+ coord = dict(point_x=10, point_y=4)
+ coordG = dict(point_x=_curves['p224'].Gx, point_y=_curves['p224'].Gy)
+
+ self.assertRaises(ValueError, ECC.construct, curve="P-224", **coord)
+ self.assertRaises(ValueError, ECC.construct, curve="P-224", d=2, **coordG)
+
+
+class TestEccModule_P256(unittest.TestCase):
+
+ def test_generate(self):
+
+ key = ECC.generate(curve="P-256")
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, EccPoint(_curves['p256'].Gx,
+ _curves['p256'].Gy) * key.d,
+ "p256")
+
+ # Other names
+ ECC.generate(curve="secp256r1")
+ ECC.generate(curve="prime256v1")
+
+ def test_construct(self):
+
+ key = ECC.construct(curve="P-256", d=1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, _curves['p256'].G)
+
+ key = ECC.construct(curve="P-256", point_x=_curves['p256'].Gx,
+ point_y=_curves['p256'].Gy)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, _curves['p256'].G)
+
+ # Other names
+ ECC.construct(curve="p256", d=1)
+ ECC.construct(curve="secp256r1", d=1)
+ ECC.construct(curve="prime256v1", d=1)
+
+ def test_negative_construct(self):
+ coord = dict(point_x=10, point_y=4)
+ coordG = dict(point_x=_curves['p256'].Gx, point_y=_curves['p256'].Gy)
+
+ self.assertRaises(ValueError, ECC.construct, curve="P-256", **coord)
+ self.assertRaises(ValueError, ECC.construct, curve="P-256", d=2, **coordG)
+
+
+class TestEccModule_P384(unittest.TestCase):
+
+ def test_generate(self):
+
+ curve = _curves['p384']
+ key = ECC.generate(curve="P-384")
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, EccPoint(curve.Gx, curve.Gy, "p384") * key.d)
+
+ # Other names
+ ECC.generate(curve="secp384r1")
+ ECC.generate(curve="prime384v1")
+
+ def test_construct(self):
+
+ curve = _curves['p384']
+ key = ECC.construct(curve="P-384", d=1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, _curves['p384'].G)
+
+ key = ECC.construct(curve="P-384", point_x=curve.Gx, point_y=curve.Gy)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, curve.G)
+
+ # Other names
+ ECC.construct(curve="p384", d=1)
+ ECC.construct(curve="secp384r1", d=1)
+ ECC.construct(curve="prime384v1", d=1)
+
+ def test_negative_construct(self):
+ coord = dict(point_x=10, point_y=4)
+ coordG = dict(point_x=_curves['p384'].Gx, point_y=_curves['p384'].Gy)
+
+ self.assertRaises(ValueError, ECC.construct, curve="P-384", **coord)
+ self.assertRaises(ValueError, ECC.construct, curve="P-384", d=2, **coordG)
+
+
+class TestEccModule_P521(unittest.TestCase):
+
+ def test_generate(self):
+
+ curve = _curves['p521']
+ key = ECC.generate(curve="P-521")
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, EccPoint(curve.Gx, curve.Gy, "p521") * key.d)
+
+ # Other names
+ ECC.generate(curve="secp521r1")
+ ECC.generate(curve="prime521v1")
+
+ def test_construct(self):
+
+ curve = _curves['p521']
+ key = ECC.construct(curve="P-521", d=1)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.pointQ, _curves['p521'].G)
+
+ key = ECC.construct(curve="P-521", point_x=curve.Gx, point_y=curve.Gy)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.pointQ, curve.G)
+
+ # Other names
+ ECC.construct(curve="p521", d=1)
+ ECC.construct(curve="secp521r1", d=1)
+ ECC.construct(curve="prime521v1", d=1)
+
+ def test_negative_construct(self):
+ coord = dict(point_x=10, point_y=4)
+ coordG = dict(point_x=_curves['p521'].Gx, point_y=_curves['p521'].Gy)
+
+ self.assertRaises(ValueError, ECC.construct, curve="P-521", **coord)
+ self.assertRaises(ValueError, ECC.construct, curve="P-521", d=2, **coordG)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TestEccPoint)
+ tests += list_test_cases(TestEccPoint_NIST_P192)
+ tests += list_test_cases(TestEccPoint_NIST_P224)
+ tests += list_test_cases(TestEccPoint_NIST_P256)
+ tests += list_test_cases(TestEccPoint_NIST_P384)
+ tests += list_test_cases(TestEccPoint_NIST_P521)
+ tests += list_test_cases(TestEccPoint_PAI_P192)
+ tests += list_test_cases(TestEccPoint_PAI_P224)
+ tests += list_test_cases(TestEccPoint_PAI_P256)
+ tests += list_test_cases(TestEccPoint_PAI_P384)
+ tests += list_test_cases(TestEccPoint_PAI_P521)
+ tests += list_test_cases(TestEccKey_P192)
+ tests += list_test_cases(TestEccKey_P224)
+ tests += list_test_cases(TestEccKey_P256)
+ tests += list_test_cases(TestEccKey_P384)
+ tests += list_test_cases(TestEccKey_P521)
+ tests += list_test_cases(TestEccModule_P192)
+ tests += list_test_cases(TestEccModule_P224)
+ tests += list_test_cases(TestEccModule_P256)
+ tests += list_test_cases(TestEccModule_P384)
+ tests += list_test_cases(TestEccModule_P521)
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ElGamal.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ElGamal.py
new file mode 100644
index 00000000..0b394aef
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_ElGamal.py
@@ -0,0 +1,217 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/PublicKey/test_ElGamal.py: Self-test for the ElGamal primitive
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.PublicKey.ElGamal"""
+
+__revision__ = "$Id$"
+
+import unittest
+from Crypto.SelfTest.st_common import list_test_cases, a2b_hex, b2a_hex
+from Crypto import Random
+from Crypto.PublicKey import ElGamal
+from Crypto.Util.number import bytes_to_long
+from Crypto.Util.py3compat import *
+
+class ElGamalTest(unittest.TestCase):
+
+ #
+ # Test vectors
+ #
+ # There seem to be no real ElGamal test vectors available in the
+ # public domain. The following test vectors have been generated
+ # with libgcrypt 1.5.0.
+ #
+ # Encryption
+ tve=[
+ {
+ # 256 bits
+ 'p' :'BA4CAEAAED8CBE952AFD2126C63EB3B345D65C2A0A73D2A3AD4138B6D09BD933',
+ 'g' :'05',
+ 'y' :'60D063600ECED7C7C55146020E7A31C4476E9793BEAED420FEC9E77604CAE4EF',
+ 'x' :'1D391BA2EE3C37FE1BA175A69B2C73A11238AD77675932',
+ 'k' :'F5893C5BAB4131264066F57AB3D8AD89E391A0B68A68A1',
+ 'pt' :'48656C6C6F207468657265',
+ 'ct1':'32BFD5F487966CEA9E9356715788C491EC515E4ED48B58F0F00971E93AAA5EC7',
+ 'ct2':'7BE8FBFF317C93E82FCEF9BD515284BA506603FEA25D01C0CB874A31F315EE68'
+ },
+
+ {
+ # 512 bits
+ 'p' :'F1B18AE9F7B4E08FDA9A04832F4E919D89462FD31BF12F92791A93519F75076D6CE3942689CDFF2F344CAFF0F82D01864F69F3AECF566C774CBACF728B81A227',
+ 'g' :'07',
+ 'y' :'688628C676E4F05D630E1BE39D0066178CA7AA83836B645DE5ADD359B4825A12B02EF4252E4E6FA9BEC1DB0BE90F6D7C8629CABB6E531F472B2664868156E20C',
+ 'x' :'14E60B1BDFD33436C0DA8A22FDC14A2CCDBBED0627CE68',
+ 'k' :'38DBF14E1F319BDA9BAB33EEEADCAF6B2EA5250577ACE7',
+ 'pt' :'48656C6C6F207468657265',
+ 'ct1':'290F8530C2CC312EC46178724F196F308AD4C523CEABB001FACB0506BFED676083FE0F27AC688B5C749AB3CB8A80CD6F7094DBA421FB19442F5A413E06A9772B',
+ 'ct2':'1D69AAAD1DC50493FB1B8E8721D621D683F3BF1321BE21BC4A43E11B40C9D4D9C80DE3AAC2AB60D31782B16B61112E68220889D53C4C3136EE6F6CE61F8A23A0'
+ }
+ ]
+
+ # Signature
+ tvs=[
+ {
+ # 256 bits
+ 'p' :'D2F3C41EA66530838A704A48FFAC9334F4701ECE3A97CEE4C69DD01AE7129DD7',
+ 'g' :'05',
+ 'y' :'C3F9417DC0DAFEA6A05C1D2333B7A95E63B3F4F28CC962254B3256984D1012E7',
+ 'x' :'165E4A39BE44D5A2D8B1332D416BC559616F536BC735BB',
+ 'k' :'C7F0C794A7EAD726E25A47FF8928013680E73C51DD3D7D99BFDA8F492585928F',
+ 'h' :'48656C6C6F207468657265',
+ 'sig1':'35CA98133779E2073EF31165AFCDEB764DD54E96ADE851715495F9C635E1E7C2',
+ 'sig2':'0135B88B1151279FE5D8078D4FC685EE81177EE9802AB123A73925FC1CB059A7',
+ },
+ {
+ # 512 bits
+ 'p' :'E24CF3A4B8A6AF749DCA6D714282FE4AABEEE44A53BB6ED15FBE32B5D3C3EF9CC4124A2ECA331F3C1C1B667ACA3766825217E7B5F9856648D95F05330C6A19CF',
+ 'g' :'0B',
+ 'y' :'2AD3A1049CA5D4ED207B2431C79A8719BB4073D4A94E450EA6CEE8A760EB07ADB67C0D52C275EE85D7B52789061EE45F2F37D9B2AE522A51C28329766BFE68AC',
+ 'x' :'16CBB4F46D9ECCF24FF9F7E63CAA3BD8936341555062AB',
+ 'k' :'8A3D89A4E429FD2476D7D717251FB79BF900FFE77444E6BB8299DC3F84D0DD57ABAB50732AE158EA52F5B9E7D8813E81FD9F79470AE22F8F1CF9AEC820A78C69',
+ 'h' :'48656C6C6F207468657265',
+ 'sig1':'BE001AABAFFF976EC9016198FBFEA14CBEF96B000CCC0063D3324016F9E91FE80D8F9325812ED24DDB2B4D4CF4430B169880B3CE88313B53255BD4EC0378586F',
+ 'sig2':'5E266F3F837BA204E3BBB6DBECC0611429D96F8C7CE8F4EFDF9D4CB681C2A954468A357BF4242CEC7418B51DFC081BCD21299EF5B5A0DDEF3A139A1817503DDE',
+ }
+ ]
+
+ def test_generate_180(self):
+ self._test_random_key(180)
+
+ def test_encryption(self):
+ for tv in self.tve:
+ d = self.convert_tv(tv, True)
+ key = ElGamal.construct(d['key'])
+ ct = key._encrypt(d['pt'], d['k'])
+ self.assertEqual(ct[0], d['ct1'])
+ self.assertEqual(ct[1], d['ct2'])
+
+ def test_decryption(self):
+ for tv in self.tve:
+ d = self.convert_tv(tv, True)
+ key = ElGamal.construct(d['key'])
+ pt = key._decrypt((d['ct1'], d['ct2']))
+ self.assertEqual(pt, d['pt'])
+
+ def test_signing(self):
+ for tv in self.tvs:
+ d = self.convert_tv(tv, True)
+ key = ElGamal.construct(d['key'])
+ sig1, sig2 = key._sign(d['h'], d['k'])
+ self.assertEqual(sig1, d['sig1'])
+ self.assertEqual(sig2, d['sig2'])
+
+ def test_verification(self):
+ for tv in self.tvs:
+ d = self.convert_tv(tv, True)
+ key = ElGamal.construct(d['key'])
+ # Positive test
+ res = key._verify( d['h'], (d['sig1'],d['sig2']) )
+ self.assertTrue(res)
+ # Negative test
+ res = key._verify( d['h'], (d['sig1']+1,d['sig2']) )
+ self.assertFalse(res)
+
+ def test_bad_key3(self):
+ tup = tup0 = list(self.convert_tv(self.tvs[0], 1)['key'])[:3]
+ tup[0] += 1 # p += 1 (not prime)
+ self.assertRaises(ValueError, ElGamal.construct, tup)
+
+ tup = tup0
+ tup[1] = 1 # g = 1
+ self.assertRaises(ValueError, ElGamal.construct, tup)
+
+ tup = tup0
+ tup[2] = tup[0]*2 # y = 2*p
+ self.assertRaises(ValueError, ElGamal.construct, tup)
+
+ def test_bad_key4(self):
+ tup = tup0 = list(self.convert_tv(self.tvs[0], 1)['key'])
+ tup[3] += 1 # x += 1
+ self.assertRaises(ValueError, ElGamal.construct, tup)
+
+ def convert_tv(self, tv, as_longs=0):
+ """Convert a test vector from textual form (hexadecimal ascii
+ to either integers or byte strings."""
+ key_comps = 'p','g','y','x'
+ tv2 = {}
+ for c in tv.keys():
+ tv2[c] = a2b_hex(tv[c])
+ if as_longs or c in key_comps or c in ('sig1','sig2'):
+ tv2[c] = bytes_to_long(tv2[c])
+ tv2['key']=[]
+ for c in key_comps:
+ tv2['key'] += [tv2[c]]
+ del tv2[c]
+ return tv2
+
+ def _test_random_key(self, bits):
+ elgObj = ElGamal.generate(bits, Random.new().read)
+ self._check_private_key(elgObj)
+ self._exercise_primitive(elgObj)
+ pub = elgObj.publickey()
+ self._check_public_key(pub)
+ self._exercise_public_primitive(elgObj)
+
+ def _check_private_key(self, elgObj):
+
+ # Check capabilities
+ self.assertTrue(elgObj.has_private())
+
+ # Sanity check key data
+ self.assertTrue(1
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.PublicKey.RSA"""
+
+__revision__ = "$Id$"
+
+import os
+import pickle
+from pickle import PicklingError
+from Crypto.Util.py3compat import *
+
+import unittest
+from Crypto.SelfTest.st_common import list_test_cases, a2b_hex, b2a_hex
+
+class RSATest(unittest.TestCase):
+ # Test vectors from "RSA-OAEP and RSA-PSS test vectors (.zip file)"
+ # ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1-vec.zip
+ # See RSADSI's PKCS#1 page at
+ # http://www.rsa.com/rsalabs/node.asp?id=2125
+
+ # from oaep-int.txt
+
+ # TODO: PyCrypto treats the message as starting *after* the leading "00"
+ # TODO: That behaviour should probably be changed in the future.
+ plaintext = """
+ eb 7a 19 ac e9 e3 00 63 50 e3 29 50 4b 45 e2
+ ca 82 31 0b 26 dc d8 7d 5c 68 f1 ee a8 f5 52 67
+ c3 1b 2e 8b b4 25 1f 84 d7 e0 b2 c0 46 26 f5 af
+ f9 3e dc fb 25 c9 c2 b3 ff 8a e1 0e 83 9a 2d db
+ 4c dc fe 4f f4 77 28 b4 a1 b7 c1 36 2b aa d2 9a
+ b4 8d 28 69 d5 02 41 21 43 58 11 59 1b e3 92 f9
+ 82 fb 3e 87 d0 95 ae b4 04 48 db 97 2f 3a c1 4f
+ 7b c2 75 19 52 81 ce 32 d2 f1 b7 6d 4d 35 3e 2d
+ """
+
+ ciphertext = """
+ 12 53 e0 4d c0 a5 39 7b b4 4a 7a b8 7e 9b f2 a0
+ 39 a3 3d 1e 99 6f c8 2a 94 cc d3 00 74 c9 5d f7
+ 63 72 20 17 06 9e 52 68 da 5d 1c 0b 4f 87 2c f6
+ 53 c1 1d f8 23 14 a6 79 68 df ea e2 8d ef 04 bb
+ 6d 84 b1 c3 1d 65 4a 19 70 e5 78 3b d6 eb 96 a0
+ 24 c2 ca 2f 4a 90 fe 9f 2e f5 c9 c1 40 e5 bb 48
+ da 95 36 ad 87 00 c8 4f c9 13 0a de a7 4e 55 8d
+ 51 a7 4d df 85 d8 b5 0d e9 68 38 d6 06 3e 09 55
+ """
+
+ modulus = """
+ bb f8 2f 09 06 82 ce 9c 23 38 ac 2b 9d a8 71 f7
+ 36 8d 07 ee d4 10 43 a4 40 d6 b6 f0 74 54 f5 1f
+ b8 df ba af 03 5c 02 ab 61 ea 48 ce eb 6f cd 48
+ 76 ed 52 0d 60 e1 ec 46 19 71 9d 8a 5b 8b 80 7f
+ af b8 e0 a3 df c7 37 72 3e e6 b4 b7 d9 3a 25 84
+ ee 6a 64 9d 06 09 53 74 88 34 b2 45 45 98 39 4e
+ e0 aa b1 2d 7b 61 a5 1f 52 7a 9a 41 f6 c1 68 7f
+ e2 53 72 98 ca 2a 8f 59 46 f8 e5 fd 09 1d bd cb
+ """
+
+ e = 0x11 # public exponent
+
+ prime_factor = """
+ c9 7f b1 f0 27 f4 53 f6 34 12 33 ea aa d1 d9 35
+ 3f 6c 42 d0 88 66 b1 d0 5a 0f 20 35 02 8b 9d 86
+ 98 40 b4 16 66 b4 2e 92 ea 0d a3 b4 32 04 b5 cf
+ ce 33 52 52 4d 04 16 a5 a4 41 e7 00 af 46 15 03
+ """
+
+ def setUp(self):
+ global RSA, Random, bytes_to_long
+ from Crypto.PublicKey import RSA
+ from Crypto import Random
+ from Crypto.Util.number import bytes_to_long, inverse
+ self.n = bytes_to_long(a2b_hex(self.modulus))
+ self.p = bytes_to_long(a2b_hex(self.prime_factor))
+
+ # Compute q, d, and u from n, e, and p
+ self.q = self.n // self.p
+ self.d = inverse(self.e, (self.p-1)*(self.q-1))
+ self.u = inverse(self.p, self.q) # u = e**-1 (mod q)
+
+ self.rsa = RSA
+
+ def test_generate_1arg(self):
+ """RSA (default implementation) generated key (1 argument)"""
+ rsaObj = self.rsa.generate(1024)
+ self._check_private_key(rsaObj)
+ self._exercise_primitive(rsaObj)
+ pub = rsaObj.public_key()
+ self._check_public_key(pub)
+ self._exercise_public_primitive(rsaObj)
+
+ def test_generate_2arg(self):
+ """RSA (default implementation) generated key (2 arguments)"""
+ rsaObj = self.rsa.generate(1024, Random.new().read)
+ self._check_private_key(rsaObj)
+ self._exercise_primitive(rsaObj)
+ pub = rsaObj.public_key()
+ self._check_public_key(pub)
+ self._exercise_public_primitive(rsaObj)
+
+ def test_generate_3args(self):
+ rsaObj = self.rsa.generate(1024, Random.new().read,e=65537)
+ self._check_private_key(rsaObj)
+ self._exercise_primitive(rsaObj)
+ pub = rsaObj.public_key()
+ self._check_public_key(pub)
+ self._exercise_public_primitive(rsaObj)
+ self.assertEqual(65537,rsaObj.e)
+
+ def test_construct_2tuple(self):
+ """RSA (default implementation) constructed key (2-tuple)"""
+ pub = self.rsa.construct((self.n, self.e))
+ self._check_public_key(pub)
+ self._check_encryption(pub)
+
+ def test_construct_3tuple(self):
+ """RSA (default implementation) constructed key (3-tuple)"""
+ rsaObj = self.rsa.construct((self.n, self.e, self.d))
+ self._check_encryption(rsaObj)
+ self._check_decryption(rsaObj)
+
+ def test_construct_4tuple(self):
+ """RSA (default implementation) constructed key (4-tuple)"""
+ rsaObj = self.rsa.construct((self.n, self.e, self.d, self.p))
+ self._check_encryption(rsaObj)
+ self._check_decryption(rsaObj)
+
+ def test_construct_5tuple(self):
+ """RSA (default implementation) constructed key (5-tuple)"""
+ rsaObj = self.rsa.construct((self.n, self.e, self.d, self.p, self.q))
+ self._check_private_key(rsaObj)
+ self._check_encryption(rsaObj)
+ self._check_decryption(rsaObj)
+
+ def test_construct_6tuple(self):
+ """RSA (default implementation) constructed key (6-tuple)"""
+ rsaObj = self.rsa.construct((self.n, self.e, self.d, self.p, self.q, self.u))
+ self._check_private_key(rsaObj)
+ self._check_encryption(rsaObj)
+ self._check_decryption(rsaObj)
+
+ def test_construct_bad_key2(self):
+ tup = (self.n, 1)
+ self.assertRaises(ValueError, self.rsa.construct, tup)
+
+ # An even modulus is wrong
+ tup = (self.n+1, self.e)
+ self.assertRaises(ValueError, self.rsa.construct, tup)
+
+ def test_construct_bad_key3(self):
+ tup = (self.n, self.e, self.d+1)
+ self.assertRaises(ValueError, self.rsa.construct, tup)
+
+ def test_construct_bad_key5(self):
+ tup = (self.n, self.e, self.d, self.p, self.p)
+ self.assertRaises(ValueError, self.rsa.construct, tup)
+
+ tup = (self.p*self.p, self.e, self.p, self.p)
+ self.assertRaises(ValueError, self.rsa.construct, tup)
+
+ tup = (self.p*self.p, 3, self.p, self.q)
+ self.assertRaises(ValueError, self.rsa.construct, tup)
+
+ def test_construct_bad_key6(self):
+ tup = (self.n, self.e, self.d, self.p, self.q, 10)
+ self.assertRaises(ValueError, self.rsa.construct, tup)
+
+ from Crypto.Util.number import inverse
+ tup = (self.n, self.e, self.d, self.p, self.q, inverse(self.q, self.p))
+ self.assertRaises(ValueError, self.rsa.construct, tup)
+
+ def test_factoring(self):
+ rsaObj = self.rsa.construct([self.n, self.e, self.d])
+ self.assertTrue(rsaObj.p==self.p or rsaObj.p==self.q)
+ self.assertTrue(rsaObj.q==self.p or rsaObj.q==self.q)
+ self.assertTrue(rsaObj.q*rsaObj.p == self.n)
+
+ self.assertRaises(ValueError, self.rsa.construct, [self.n, self.e, self.n-1])
+
+ def test_repr(self):
+ rsaObj = self.rsa.construct((self.n, self.e, self.d, self.p, self.q))
+ repr(rsaObj)
+
+ def test_serialization(self):
+ """RSA keys are unpickable"""
+
+ rsa_key = self.rsa.generate(1024)
+ self.assertRaises(PicklingError, pickle.dumps, rsa_key)
+
+ def test_raw_rsa_boundary(self):
+ # The argument of every RSA raw operation (encrypt/decrypt) must be
+ # non-negative and no larger than the modulus
+ rsa_obj = self.rsa.generate(1024)
+
+ self.assertRaises(ValueError, rsa_obj._decrypt, rsa_obj.n)
+ self.assertRaises(ValueError, rsa_obj._decrypt_to_bytes, rsa_obj.n)
+ self.assertRaises(ValueError, rsa_obj._encrypt, rsa_obj.n)
+
+ self.assertRaises(ValueError, rsa_obj._decrypt, -1)
+ self.assertRaises(ValueError, rsa_obj._decrypt_to_bytes, -1)
+ self.assertRaises(ValueError, rsa_obj._encrypt, -1)
+
+ def test_size(self):
+ pub = self.rsa.construct((self.n, self.e))
+ self.assertEqual(pub.size_in_bits(), 1024)
+ self.assertEqual(pub.size_in_bytes(), 128)
+
+ def _check_private_key(self, rsaObj):
+ from Crypto.Math.Numbers import Integer
+
+ # Check capabilities
+ self.assertEqual(1, rsaObj.has_private())
+
+ # Sanity check key data
+ self.assertEqual(rsaObj.n, rsaObj.p * rsaObj.q) # n = pq
+ lcm = int(Integer(rsaObj.p-1).lcm(rsaObj.q-1))
+ self.assertEqual(1, rsaObj.d * rsaObj.e % lcm) # ed = 1 (mod LCM(p-1, q-1))
+ self.assertEqual(1, rsaObj.p * rsaObj.u % rsaObj.q) # pu = 1 (mod q)
+ self.assertEqual(1, rsaObj.p > 1) # p > 1
+ self.assertEqual(1, rsaObj.q > 1) # q > 1
+ self.assertEqual(1, rsaObj.e > 1) # e > 1
+ self.assertEqual(1, rsaObj.d > 1) # d > 1
+
+ self.assertEqual(rsaObj.u, rsaObj.invp)
+ self.assertEqual(1, rsaObj.q * rsaObj.invq % rsaObj.p)
+
+ def _check_public_key(self, rsaObj):
+ ciphertext = a2b_hex(self.ciphertext)
+
+ # Check capabilities
+ self.assertEqual(0, rsaObj.has_private())
+
+ # Check rsaObj.[ne] -> rsaObj.[ne] mapping
+ self.assertEqual(rsaObj.n, rsaObj.n)
+ self.assertEqual(rsaObj.e, rsaObj.e)
+
+ # Check that private parameters are all missing
+ self.assertEqual(0, hasattr(rsaObj, 'd'))
+ self.assertEqual(0, hasattr(rsaObj, 'p'))
+ self.assertEqual(0, hasattr(rsaObj, 'q'))
+ self.assertEqual(0, hasattr(rsaObj, 'u'))
+
+ # Sanity check key data
+ self.assertEqual(1, rsaObj.e > 1) # e > 1
+
+ # Public keys should not be able to sign or decrypt
+ self.assertRaises(TypeError, rsaObj._decrypt,
+ bytes_to_long(ciphertext))
+ self.assertRaises(TypeError, rsaObj._decrypt_to_bytes,
+ bytes_to_long(ciphertext))
+
+ # Check __eq__ and __ne__
+ self.assertEqual(rsaObj.public_key() == rsaObj.public_key(),True) # assert_
+ self.assertEqual(rsaObj.public_key() != rsaObj.public_key(),False) # assertFalse
+
+ self.assertEqual(rsaObj.publickey(), rsaObj.public_key())
+
+ def _exercise_primitive(self, rsaObj):
+ # Since we're using a randomly-generated key, we can't check the test
+ # vector, but we can make sure encryption and decryption are inverse
+ # operations.
+ ciphertext = bytes_to_long(a2b_hex(self.ciphertext))
+
+ # Test decryption
+ plaintext = rsaObj._decrypt(ciphertext)
+
+ # Test encryption (2 arguments)
+ new_ciphertext2 = rsaObj._encrypt(plaintext)
+ self.assertEqual(ciphertext, new_ciphertext2)
+
+ def _exercise_public_primitive(self, rsaObj):
+ plaintext = a2b_hex(self.plaintext)
+
+ # Test encryption (2 arguments)
+ new_ciphertext2 = rsaObj._encrypt(bytes_to_long(plaintext))
+
+ def _check_encryption(self, rsaObj):
+ plaintext = a2b_hex(self.plaintext)
+ ciphertext = a2b_hex(self.ciphertext)
+
+ # Test encryption
+ new_ciphertext2 = rsaObj._encrypt(bytes_to_long(plaintext))
+ self.assertEqual(bytes_to_long(ciphertext), new_ciphertext2)
+
+ def _check_decryption(self, rsaObj):
+ plaintext = bytes_to_long(a2b_hex(self.plaintext))
+ ciphertext = bytes_to_long(a2b_hex(self.ciphertext))
+
+ # Test plain decryption
+ new_plaintext = rsaObj._decrypt(ciphertext)
+ self.assertEqual(plaintext, new_plaintext)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(RSATest)
+ return tests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_Curve25519.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_Curve25519.py
new file mode 100644
index 00000000..71cd1629
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_Curve25519.py
@@ -0,0 +1,385 @@
+# ===================================================================
+#
+# Copyright (c) 2024, Helder Eijs
+# 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 os
+import errno
+import warnings
+import unittest
+from binascii import unhexlify
+from unittest import SkipTest
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Util.py3compat import tostr, FileNotFoundError
+from Crypto.Util.asn1 import DerSequence, DerBitString
+from Crypto.Hash import SHAKE128
+
+from Crypto.PublicKey import ECC
+
+try:
+ import pycryptodome_test_vectors # type: ignore
+ test_vectors_available = True
+except ImportError:
+ test_vectors_available = False
+
+
+def load_file(file_name, mode="rb"):
+ results = None
+
+ try:
+ if not test_vectors_available:
+ raise FileNotFoundError(errno.ENOENT,
+ os.strerror(errno.ENOENT),
+ file_name)
+
+ dir_comps = ("PublicKey", "ECC")
+ init_dir = os.path.dirname(pycryptodome_test_vectors.__file__)
+ full_file_name = os.path.join(os.path.join(init_dir, *dir_comps), file_name)
+ with open(full_file_name, mode) as file_in:
+ results = file_in.read()
+
+ except FileNotFoundError:
+ warnings.warn("Warning: skipping extended tests for ECC",
+ UserWarning,
+ stacklevel=2)
+
+ if results is None:
+ raise SkipTest("Missing %s" % file_name)
+
+ return results
+
+
+def compact(lines):
+ ext = b"".join(lines)
+ return unhexlify(tostr(ext).replace(" ", "").replace(":", ""))
+
+
+def create_ref_keys_x25519():
+ key_lines = load_file("ecc_x25519.txt").splitlines()
+ seed = compact(key_lines[5:8])
+ key = ECC.construct(curve="Curve25519", seed=seed)
+ return (key, key.public_key())
+
+
+def get_fixed_prng():
+ return SHAKE128.new().update(b"SEED").read
+
+
+def extract_bitstring_from_spki(data):
+ seq = DerSequence()
+ seq.decode(data)
+ bs = DerBitString()
+ bs.decode(seq[1])
+ return bs.value
+
+
+class TestImport(unittest.TestCase):
+
+ def test_empty(self):
+ self.assertRaises(ValueError, ECC.import_key, b"")
+
+ def test_mismatch(self):
+ # Private key with X448 Object ID but X25519 key
+ mismatch_hex = "302e020100300506032b656f042204207009906b64ec727d5cb5c23007bf0425b3fd79014c6cd62ca3dddfcf0f278f79"
+ mismatch = unhexlify(mismatch_hex)
+ self.assertRaises(ValueError, ECC.import_key, mismatch)
+
+
+class TestImport_Curve25519(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestImport_Curve25519, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_x25519()
+
+ def test_import_public_der(self):
+ key_file = load_file("ecc_x25519_public.der")
+
+ key = ECC._import_subjectPublicKeyInfo(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_pkcs8_der(self):
+ key_file = load_file("ecc_x25519_private.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_1(self):
+ key_file = load_file("ecc_x25519_private_p8.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_2(self):
+ key_file = load_file("ecc_x25519_private_p8.pem")
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_3(self):
+ key_file = load_file("ecc_x25519_private_p8_2.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_der(self):
+ key_file = load_file("ecc_x25519_x509.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_public_pem(self):
+ key_file = load_file("ecc_x25519_public.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_private_pem(self):
+ key_file = load_file("ecc_x25519_private.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pem_encrypted(self):
+ for algo in "des3", "aes128", "aes192", "aes256":
+ key_file = load_file("ecc_x25519_private_enc_%s.pem" % algo)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(tostr(key_file), b"secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_pem(self):
+ key_file = load_file("ecc_x25519_x509.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+
+class TestExport_Curve25519(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestExport_Curve25519, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_x25519()
+
+ def test_export_public_der(self):
+ key_file = load_file("ecc_x25519_public.der")
+
+ encoded = self.ref_public._export_subjectPublicKeyInfo(True)
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_clear(self):
+ key_file = load_file("ecc_x25519_private.der")
+
+ encoded = self.ref_private._export_pkcs8()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ self.assertRaises(ValueError, self.ref_private.export_key,
+ format="DER", use_pkcs8=False)
+
+ def test_export_private_pkcs8_encrypted(self):
+ encoded = self.ref_private._export_pkcs8(passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC._import_pkcs8, encoded, None)
+
+ decoded = ECC._import_pkcs8(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA256AndAES128-CBC",
+ prot_params={'iteration_count': 123})
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_public_pem(self):
+ key_file_ref = load_file("ecc_x25519_public.pem", "rt").strip()
+ key_file = self.ref_public.export_key(format="PEM").strip()
+ self.assertEqual(key_file_ref, key_file)
+
+ def test_export_private_pem_clear(self):
+ key_file = load_file("ecc_x25519_private.pem", "rt").strip()
+ encoded = self.ref_private.export_key(format="PEM").strip()
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pem_encrypted(self):
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase=b"secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "ENCRYPTED PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_raw(self):
+ encoded = self.ref_public.export_key(format='raw')
+ self.assertEqual(len(encoded), 32)
+ self.assertEqual(encoded, unhexlify(b'ff7561ef60c9c8a757f6d6372ec14142c9be208d0e719136d8d3c715dfcf7e15'))
+
+ def test_prng(self):
+ # Test that password-protected containers use the provided PRNG
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_byte_or_string_passphrase(self):
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase=b"secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_error_params1(self):
+ # Unknown format
+ self.assertRaises(ValueError, self.ref_private.export_key, format="XXX")
+
+ # Missing 'protection' parameter when PKCS#8 is used
+ self.assertRaises(ValueError,
+ self.ref_private.export_key,
+ format="PEM",
+ passphrase="secret")
+
+ # Empty password
+ self.assertRaises(ValueError,
+ self.ref_private.export_key,
+ format="PEM",
+ passphrase="",
+ use_pkcs8=False)
+ self.assertRaises(ValueError,
+ self.ref_private.export_key,
+ format="PEM",
+ passphrase="",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # No private keys with OpenSSH
+ self.assertRaises(ValueError,
+ self.ref_private.export_key,
+ format="OpenSSH",
+ passphrase="secret")
+
+
+class TestImport_Curve25519_Weak(unittest.TestCase):
+
+ def test_weak_pem(self):
+
+ p = 2**255 - 19
+ weak_x = (0,
+ 1,
+ 325606250916557431795983626356110631294008115727848805560023387167927233504,
+ 39382357235489614581723060781553021112529911719440698176882885853963445705823,
+ p - 1,
+ p,
+ p + 1,
+ p + 325606250916557431795983626356110631294008115727848805560023387167927233504,
+ p + 39382357235489614581723060781553021112529911719440698176882885853963445705823,
+ p * 2 - 1,
+ p * 2,
+ p * 2 + 1)
+
+ for x in weak_x:
+ low_order_point = ECC.EccXPoint(x, "curve25519")
+ weak_key = ECC.EccKey(point=low_order_point, curve="curve25519")
+ encoded = weak_key.export_key(format="PEM")
+
+ self.assertRaises(ValueError,
+ ECC.import_key,
+ encoded)
+
+
+def get_tests(config={}):
+ tests = []
+ try:
+ tests += list_test_cases(TestImport)
+ tests += list_test_cases(TestImport_Curve25519)
+ tests += list_test_cases(TestExport_Curve25519)
+ tests += list_test_cases(TestImport_Curve25519_Weak)
+ except SkipTest:
+ pass
+ return tests
+
+
+if __name__ == '__main__':
+ def suit():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_Curve448.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_Curve448.py
new file mode 100644
index 00000000..e2fdb171
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_Curve448.py
@@ -0,0 +1,351 @@
+# This file is licensed under the BSD 2-Clause License.
+# See https://opensource.org/licenses/BSD-2-Clause for details.
+
+import os
+import errno
+import warnings
+import unittest
+from binascii import unhexlify
+from unittest import SkipTest
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Util.py3compat import tostr, FileNotFoundError
+from Crypto.Util.asn1 import DerSequence, DerBitString
+from Crypto.Hash import SHAKE128
+
+from Crypto.PublicKey import ECC
+
+try:
+ import pycryptodome_test_vectors # type: ignore
+ test_vectors_available = True
+except ImportError:
+ test_vectors_available = False
+
+
+def load_file(file_name, mode="rb"):
+ results = None
+
+ try:
+ if not test_vectors_available:
+ raise FileNotFoundError(errno.ENOENT,
+ os.strerror(errno.ENOENT),
+ file_name)
+
+ dir_comps = ("PublicKey", "ECC")
+ init_dir = os.path.dirname(pycryptodome_test_vectors.__file__)
+ full_file_name = os.path.join(os.path.join(init_dir, *dir_comps), file_name)
+ with open(full_file_name, mode) as file_in:
+ results = file_in.read()
+
+ except FileNotFoundError:
+ warnings.warn("Warning: skipping extended tests for ECC",
+ UserWarning,
+ stacklevel=2)
+
+ if results is None:
+ raise SkipTest("Missing %s" % file_name)
+
+ return results
+
+
+def compact(lines):
+ ext = b"".join(lines)
+ return unhexlify(tostr(ext).replace(" ", "").replace(":", ""))
+
+
+def create_ref_keys_x448():
+ key_lines = load_file("ecc_x448.txt").splitlines()
+ seed = compact(key_lines[6:10])
+ key = ECC.construct(curve="Curve448", seed=seed)
+ return (key, key.public_key())
+
+
+def get_fixed_prng():
+ return SHAKE128.new().update(b"SEED").read
+
+
+def extract_bitstring_from_spki(data):
+ seq = DerSequence()
+ seq.decode(data)
+ bs = DerBitString()
+ bs.decode(seq[1])
+ return bs.value
+
+
+class TestImport(unittest.TestCase):
+
+ def test_empty(self):
+ self.assertRaises(ValueError, ECC.import_key, b"")
+
+ def test_mismatch(self):
+ # Private key with X448 Object ID but X448 key
+ mismatch_hex = "302e020100300506032b656f042204207009906b64ec727d5cb5c23007bf0425b3fd79014c6cd62ca3dddfcf0f278f79"
+ mismatch = unhexlify(mismatch_hex)
+ self.assertRaises(ValueError, ECC.import_key, mismatch)
+
+
+class TestImport_Curve448(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestImport_Curve448, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_x448()
+
+ def test_import_public_der(self):
+ key_file = load_file("ecc_x448_public.der")
+
+ key = ECC._import_subjectPublicKeyInfo(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_pkcs8_der(self):
+ key_file = load_file("ecc_x448_private.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_1(self):
+ key_file = load_file("ecc_x448_private_p8.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_2(self):
+ key_file = load_file("ecc_x448_private_p8.pem")
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_3(self):
+ key_file = load_file("ecc_x448_private_p8_2.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_der(self):
+ key_file = load_file("ecc_x448_x509.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_public_pem(self):
+ key_file = load_file("ecc_x448_public.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_private_pem(self):
+ key_file = load_file("ecc_x448_private.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pem_encrypted(self):
+ for algo in "des3", "aes128", "aes192", "aes256":
+ key_file = load_file("ecc_x448_private_enc_%s.pem" % algo)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(tostr(key_file), b"secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_pem(self):
+ key_file = load_file("ecc_x448_x509.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+
+class TestExport_Curve448(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestExport_Curve448, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_x448()
+
+ def test_export_public_der(self):
+ key_file = load_file("ecc_x448_public.der")
+
+ encoded = self.ref_public._export_subjectPublicKeyInfo(True)
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_clear(self):
+ key_file = load_file("ecc_x448_private.der")
+
+ encoded = self.ref_private._export_pkcs8()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ self.assertRaises(ValueError, self.ref_private.export_key,
+ format="DER", use_pkcs8=False)
+
+ def test_export_private_pkcs8_encrypted(self):
+ encoded = self.ref_private._export_pkcs8(passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC._import_pkcs8, encoded, None)
+
+ decoded = ECC._import_pkcs8(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA256AndAES128-CBC",
+ prot_params={'iteration_count': 123})
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_public_pem(self):
+ key_file_ref = load_file("ecc_x448_public.pem", "rt").strip()
+ key_file = self.ref_public.export_key(format="PEM").strip()
+ self.assertEqual(key_file_ref, key_file)
+
+ def test_export_private_pem_clear(self):
+ key_file = load_file("ecc_x448_private.pem", "rt").strip()
+ encoded = self.ref_private.export_key(format="PEM").strip()
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pem_encrypted(self):
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase=b"secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "ENCRYPTED PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_raw(self):
+ encoded = self.ref_public.export_key(format='raw')
+ self.assertEqual(len(encoded), 56)
+ self.assertEqual(encoded, unhexlify(b'e2abae24ab8f65b01969e61f84fee615b525f413a90e3d727f71d0ffe60fb1d0a1a0285f2a7fd88789206e0aa4f3e9fcb9e4ba5d644e691e'))
+
+ def test_prng(self):
+ # Test that password-protected containers use the provided PRNG
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_byte_or_string_passphrase(self):
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase=b"secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_error_params1(self):
+ # Unknown format
+ self.assertRaises(ValueError, self.ref_private.export_key, format="XXX")
+
+ # Missing 'protection' parameter when PKCS#8 is used
+ self.assertRaises(ValueError,
+ self.ref_private.export_key,
+ format="PEM",
+ passphrase="secret")
+
+ # Empty password
+ self.assertRaises(ValueError,
+ self.ref_private.export_key,
+ format="PEM",
+ passphrase="",
+ use_pkcs8=False)
+ self.assertRaises(ValueError,
+ self.ref_private.export_key,
+ format="PEM",
+ passphrase="",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # No private keys with OpenSSH
+ self.assertRaises(ValueError,
+ self.ref_private.export_key,
+ format="OpenSSH",
+ passphrase="secret")
+
+
+class TestImport_Curve448_Weak(unittest.TestCase):
+
+ def test_weak_pem(self):
+
+ p = 2**448 - 2**224 - 1
+ weak_x = (0,
+ 1,
+ p - 1,
+ p,
+ p + 1)
+
+ for x in weak_x:
+ low_order_point = ECC.EccXPoint(x, "curve448")
+ weak_key = ECC.EccKey(point=low_order_point, curve="curve448")
+ encoded = weak_key.export_key(format="PEM")
+
+ self.assertRaises(ValueError,
+ ECC.import_key,
+ encoded)
+
+
+def get_tests(config={}):
+ tests = []
+ try:
+ tests += list_test_cases(TestImport)
+ tests += list_test_cases(TestImport_Curve448)
+ tests += list_test_cases(TestExport_Curve448)
+ tests += list_test_cases(TestImport_Curve448_Weak)
+ except SkipTest:
+ pass
+ return tests
+
+
+if __name__ == '__main__':
+ def suit():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_DSA.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_DSA.py
new file mode 100644
index 00000000..266b46f0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_DSA.py
@@ -0,0 +1,554 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/PublicKey/test_import_DSA.py: Self-test for importing DSA keys
+#
+# ===================================================================
+# 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 unittest
+import re
+
+from Crypto.PublicKey import DSA
+from Crypto.SelfTest.st_common import *
+from Crypto.Util.py3compat import *
+
+from binascii import unhexlify
+
+class ImportKeyTests(unittest.TestCase):
+
+ y = 92137165128186062214622779787483327510946462589285775188003362705875131352591574106484271700740858696583623951844732128165434284507709057439633739849986759064015013893156866539696757799934634945787496920169462601722830899660681779448742875054459716726855443681559131362852474817534616736104831095601710736729
+ p = 162452170958135306109773853318304545923250830605675936228618290525164105310663722368377131295055868997377338797580997938253236213714988311430600065853662861806894003694743806769284131194035848116051021923956699231855223389086646903420682639786976554552864568460372266462812137447840653688476258666833303658691
+ q = 988791743931120302950649732173330531512663554851
+ g = 85583152299197514738065570254868711517748965097380456700369348466136657764813442044039878840094809620913085570225318356734366886985903212775602770761953571967834823306046501307810937486758039063386311593890777319935391363872375452381836756832784184928202587843258855704771836753434368484556809100537243908232
+ x = 540873410045082450874416847965843801027716145253
+
+ def setUp(self):
+
+ # It is easier to write test vectors in text form,
+ # and convert them to byte strigs dynamically here
+ for mname, mvalue in ImportKeyTests.__dict__.items():
+ if mname[:4] in ('der_', 'pem_', 'ssh_'):
+ if mname[:4] == 'der_':
+ mvalue = unhexlify(tobytes(mvalue))
+ mvalue = tobytes(mvalue)
+ setattr(self, mname, mvalue)
+
+ # 1. SubjectPublicKeyInfo
+ der_public=\
+ '308201b73082012b06072a8648ce3804013082011e02818100e756ee1717f4b6'+\
+ '794c7c214724a19763742c45572b4b3f8ff3b44f3be9f44ce039a2757695ec91'+\
+ '5697da74ef914fcd1b05660e2419c761d639f45d2d79b802dbd23e7ab8b81b47'+\
+ '9a380e1f30932584ba2a0b955032342ebc83cb5ca906e7b0d7cd6fe656cecb4c'+\
+ '8b5a77123a8c6750a481e3b06057aff6aa6eba620b832d60c3021500ad32f48c'+\
+ 'd3ae0c45a198a61fa4b5e20320763b2302818079dfdc3d614fe635fceb7eaeae'+\
+ '3718dc2efefb45282993ac6749dc83c223d8c1887296316b3b0b54466cf444f3'+\
+ '4b82e3554d0b90a778faaf1306f025dae6a3e36c7f93dd5bac4052b92370040a'+\
+ 'ca70b8d5820599711900efbc961812c355dd9beffe0981da85c5548074b41c56'+\
+ 'ae43fd300d89262e4efd89943f99a651b03888038185000281810083352a69a1'+\
+ '32f34843d2a0eb995bff4e2f083a73f0049d2c91ea2f0ce43d144abda48199e4'+\
+ 'b003c570a8af83303d45105f606c5c48d925a40ed9c2630c2fa4cdbf838539de'+\
+ 'b9a29f919085f2046369f627ca84b2cb1e2c7940564b670f963ab1164d4e2ca2'+\
+ 'bf6ffd39f12f548928bf4d2d1b5e6980b4f1be4c92a91986fba559'
+
+ def testImportKey1(self):
+ key_obj = DSA.importKey(self.der_public)
+ self.assertFalse(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+
+ def testExportKey1(self):
+ tup = (self.y, self.g, self.p, self.q)
+ key = DSA.construct(tup)
+ encoded = key.export_key('DER')
+ self.assertEqual(self.der_public, encoded)
+
+ # 2.
+ pem_public="""\
+-----BEGIN PUBLIC KEY-----
+MIIBtzCCASsGByqGSM44BAEwggEeAoGBAOdW7hcX9LZ5THwhRyShl2N0LEVXK0s/
+j/O0Tzvp9EzgOaJ1dpXskVaX2nTvkU/NGwVmDiQZx2HWOfRdLXm4AtvSPnq4uBtH
+mjgOHzCTJYS6KguVUDI0LryDy1ypBuew181v5lbOy0yLWncSOoxnUKSB47BgV6/2
+qm66YguDLWDDAhUArTL0jNOuDEWhmKYfpLXiAyB2OyMCgYB539w9YU/mNfzrfq6u
+NxjcLv77RSgpk6xnSdyDwiPYwYhyljFrOwtURmz0RPNLguNVTQuQp3j6rxMG8CXa
+5qPjbH+T3VusQFK5I3AECspwuNWCBZlxGQDvvJYYEsNV3Zvv/gmB2oXFVIB0tBxW
+rkP9MA2JJi5O/YmUP5mmUbA4iAOBhQACgYEAgzUqaaEy80hD0qDrmVv/Ti8IOnPw
+BJ0skeovDOQ9FEq9pIGZ5LADxXCor4MwPUUQX2BsXEjZJaQO2cJjDC+kzb+DhTne
+uaKfkZCF8gRjafYnyoSyyx4seUBWS2cPljqxFk1OLKK/b/058S9UiSi/TS0bXmmA
+tPG+TJKpGYb7pVk=
+-----END PUBLIC KEY-----"""
+
+ def testImportKey2(self):
+ for pem in (self.pem_public, tostr(self.pem_public)):
+ key_obj = DSA.importKey(pem)
+ self.assertFalse(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+
+ def testExportKey2(self):
+ tup = (self.y, self.g, self.p, self.q)
+ key = DSA.construct(tup)
+ encoded = key.export_key('PEM')
+ self.assertEqual(self.pem_public, encoded)
+
+ # 3. OpenSSL/OpenSSH format
+ der_private=\
+ '308201bb02010002818100e756ee1717f4b6794c7c214724a19763742c45572b'+\
+ '4b3f8ff3b44f3be9f44ce039a2757695ec915697da74ef914fcd1b05660e2419'+\
+ 'c761d639f45d2d79b802dbd23e7ab8b81b479a380e1f30932584ba2a0b955032'+\
+ '342ebc83cb5ca906e7b0d7cd6fe656cecb4c8b5a77123a8c6750a481e3b06057'+\
+ 'aff6aa6eba620b832d60c3021500ad32f48cd3ae0c45a198a61fa4b5e2032076'+\
+ '3b2302818079dfdc3d614fe635fceb7eaeae3718dc2efefb45282993ac6749dc'+\
+ '83c223d8c1887296316b3b0b54466cf444f34b82e3554d0b90a778faaf1306f0'+\
+ '25dae6a3e36c7f93dd5bac4052b92370040aca70b8d5820599711900efbc9618'+\
+ '12c355dd9beffe0981da85c5548074b41c56ae43fd300d89262e4efd89943f99'+\
+ 'a651b038880281810083352a69a132f34843d2a0eb995bff4e2f083a73f0049d'+\
+ '2c91ea2f0ce43d144abda48199e4b003c570a8af83303d45105f606c5c48d925'+\
+ 'a40ed9c2630c2fa4cdbf838539deb9a29f919085f2046369f627ca84b2cb1e2c'+\
+ '7940564b670f963ab1164d4e2ca2bf6ffd39f12f548928bf4d2d1b5e6980b4f1'+\
+ 'be4c92a91986fba55902145ebd9a3f0b82069d98420986b314215025756065'
+
+ def testImportKey3(self):
+ key_obj = DSA.importKey(self.der_private)
+ self.assertTrue(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+ self.assertEqual(self.x, key_obj.x)
+
+ def testExportKey3(self):
+ tup = (self.y, self.g, self.p, self.q, self.x)
+ key = DSA.construct(tup)
+ encoded = key.export_key('DER', pkcs8=False)
+ self.assertEqual(self.der_private, encoded)
+
+ # 4.
+ pem_private="""\
+-----BEGIN DSA PRIVATE KEY-----
+MIIBuwIBAAKBgQDnVu4XF/S2eUx8IUckoZdjdCxFVytLP4/ztE876fRM4DmidXaV
+7JFWl9p075FPzRsFZg4kGcdh1jn0XS15uALb0j56uLgbR5o4Dh8wkyWEuioLlVAy
+NC68g8tcqQbnsNfNb+ZWzstMi1p3EjqMZ1CkgeOwYFev9qpuumILgy1gwwIVAK0y
+9IzTrgxFoZimH6S14gMgdjsjAoGAed/cPWFP5jX8636urjcY3C7++0UoKZOsZ0nc
+g8Ij2MGIcpYxazsLVEZs9ETzS4LjVU0LkKd4+q8TBvAl2uaj42x/k91brEBSuSNw
+BArKcLjVggWZcRkA77yWGBLDVd2b7/4JgdqFxVSAdLQcVq5D/TANiSYuTv2JlD+Z
+plGwOIgCgYEAgzUqaaEy80hD0qDrmVv/Ti8IOnPwBJ0skeovDOQ9FEq9pIGZ5LAD
+xXCor4MwPUUQX2BsXEjZJaQO2cJjDC+kzb+DhTneuaKfkZCF8gRjafYnyoSyyx4s
+eUBWS2cPljqxFk1OLKK/b/058S9UiSi/TS0bXmmAtPG+TJKpGYb7pVkCFF69mj8L
+ggadmEIJhrMUIVAldWBl
+-----END DSA PRIVATE KEY-----"""
+
+ def testImportKey4(self):
+ for pem in (self.pem_private, tostr(self.pem_private)):
+ key_obj = DSA.importKey(pem)
+ self.assertTrue(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+ self.assertEqual(self.x, key_obj.x)
+
+ def testExportKey4(self):
+ tup = (self.y, self.g, self.p, self.q, self.x)
+ key = DSA.construct(tup)
+ encoded = key.export_key('PEM', pkcs8=False)
+ self.assertEqual(self.pem_private, encoded)
+
+ # 5. PKCS8 (unencrypted)
+ der_pkcs8=\
+ '3082014a0201003082012b06072a8648ce3804013082011e02818100e756ee17'+\
+ '17f4b6794c7c214724a19763742c45572b4b3f8ff3b44f3be9f44ce039a27576'+\
+ '95ec915697da74ef914fcd1b05660e2419c761d639f45d2d79b802dbd23e7ab8'+\
+ 'b81b479a380e1f30932584ba2a0b955032342ebc83cb5ca906e7b0d7cd6fe656'+\
+ 'cecb4c8b5a77123a8c6750a481e3b06057aff6aa6eba620b832d60c3021500ad'+\
+ '32f48cd3ae0c45a198a61fa4b5e20320763b2302818079dfdc3d614fe635fceb'+\
+ '7eaeae3718dc2efefb45282993ac6749dc83c223d8c1887296316b3b0b54466c'+\
+ 'f444f34b82e3554d0b90a778faaf1306f025dae6a3e36c7f93dd5bac4052b923'+\
+ '70040aca70b8d5820599711900efbc961812c355dd9beffe0981da85c5548074'+\
+ 'b41c56ae43fd300d89262e4efd89943f99a651b03888041602145ebd9a3f0b82'+\
+ '069d98420986b314215025756065'
+
+ def testImportKey5(self):
+ key_obj = DSA.importKey(self.der_pkcs8)
+ self.assertTrue(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+ self.assertEqual(self.x, key_obj.x)
+
+ def testExportKey5(self):
+ tup = (self.y, self.g, self.p, self.q, self.x)
+ key = DSA.construct(tup)
+ encoded = key.export_key('DER')
+ self.assertEqual(self.der_pkcs8, encoded)
+ encoded = key.export_key('DER', pkcs8=True)
+ self.assertEqual(self.der_pkcs8, encoded)
+
+ # 6.
+ pem_pkcs8="""\
+-----BEGIN PRIVATE KEY-----
+MIIBSgIBADCCASsGByqGSM44BAEwggEeAoGBAOdW7hcX9LZ5THwhRyShl2N0LEVX
+K0s/j/O0Tzvp9EzgOaJ1dpXskVaX2nTvkU/NGwVmDiQZx2HWOfRdLXm4AtvSPnq4
+uBtHmjgOHzCTJYS6KguVUDI0LryDy1ypBuew181v5lbOy0yLWncSOoxnUKSB47Bg
+V6/2qm66YguDLWDDAhUArTL0jNOuDEWhmKYfpLXiAyB2OyMCgYB539w9YU/mNfzr
+fq6uNxjcLv77RSgpk6xnSdyDwiPYwYhyljFrOwtURmz0RPNLguNVTQuQp3j6rxMG
+8CXa5qPjbH+T3VusQFK5I3AECspwuNWCBZlxGQDvvJYYEsNV3Zvv/gmB2oXFVIB0
+tBxWrkP9MA2JJi5O/YmUP5mmUbA4iAQWAhRevZo/C4IGnZhCCYazFCFQJXVgZQ==
+-----END PRIVATE KEY-----"""
+
+ def testImportKey6(self):
+ for pem in (self.pem_pkcs8, tostr(self.pem_pkcs8)):
+ key_obj = DSA.importKey(pem)
+ self.assertTrue(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+ self.assertEqual(self.x, key_obj.x)
+
+ def testExportKey6(self):
+ tup = (self.y, self.g, self.p, self.q, self.x)
+ key = DSA.construct(tup)
+ encoded = key.export_key('PEM')
+ self.assertEqual(self.pem_pkcs8, encoded)
+ encoded = key.export_key('PEM', pkcs8=True)
+ self.assertEqual(self.pem_pkcs8, encoded)
+
+ # 7. OpenSSH/RFC4253
+ ssh_pub="""ssh-dss AAAAB3NzaC1kc3MAAACBAOdW7hcX9LZ5THwhRyShl2N0LEVXK0s/j/O0Tzvp9EzgOaJ1dpXskVaX2nTvkU/NGwVmDiQZx2HWOfRdLXm4AtvSPnq4uBtHmjgOHzCTJYS6KguVUDI0LryDy1ypBuew181v5lbOy0yLWncSOoxnUKSB47BgV6/2qm66YguDLWDDAAAAFQCtMvSM064MRaGYph+kteIDIHY7IwAAAIB539w9YU/mNfzrfq6uNxjcLv77RSgpk6xnSdyDwiPYwYhyljFrOwtURmz0RPNLguNVTQuQp3j6rxMG8CXa5qPjbH+T3VusQFK5I3AECspwuNWCBZlxGQDvvJYYEsNV3Zvv/gmB2oXFVIB0tBxWrkP9MA2JJi5O/YmUP5mmUbA4iAAAAIEAgzUqaaEy80hD0qDrmVv/Ti8IOnPwBJ0skeovDOQ9FEq9pIGZ5LADxXCor4MwPUUQX2BsXEjZJaQO2cJjDC+kzb+DhTneuaKfkZCF8gRjafYnyoSyyx4seUBWS2cPljqxFk1OLKK/b/058S9UiSi/TS0bXmmAtPG+TJKpGYb7pVk="""
+
+ def testImportKey7(self):
+ for ssh in (self.ssh_pub, tostr(self.ssh_pub)):
+ key_obj = DSA.importKey(ssh)
+ self.assertFalse(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+
+ def testExportKey7(self):
+ tup = (self.y, self.g, self.p, self.q)
+ key = DSA.construct(tup)
+ encoded = key.export_key('OpenSSH')
+ self.assertEqual(self.ssh_pub, encoded)
+
+ # 8. Encrypted OpenSSL/OpenSSH
+ pem_private_encrypted="""\
+-----BEGIN DSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,70B6908939D65E9F2EB999E8729788CE
+
+4V6GHRDpCrdZ8MBjbyp5AlGUrjvr2Pn2e2zVxy5RBt4FBj9/pa0ae0nnyUPMLSUU
+kKyOR0topRYTVRLElm4qVrb5uNZ3hRwfbklr+pSrB7O9eHz9V5sfOQxyODS07JxK
+k1OdOs70/ouMXLF9EWfAZOmWUccZKHNblUwg1p1UrZIz5jXw4dUE/zqhvXh6d+iC
+ADsICaBCjCrRQJKDp50h3+ndQjkYBKVH+pj8TiQ79U7lAvdp3+iMghQN6YXs9mdI
+gFpWw/f97oWM4GHZFqHJ+VSMNFjBiFhAvYV587d7Lk4dhD8sCfbxj42PnfRgUItc
+nnPqHxmhMQozBWzYM4mQuo3XbF2WlsNFbOzFVyGhw1Bx1s91qvXBVWJh2ozrW0s6
+HYDV7ZkcTml/4kjA/d+mve6LZ8kuuR1qCiZx6rkffhh1gDN/1Xz3HVvIy/dQ+h9s
+5zp7PwUoWbhqp3WCOr156P6gR8qo7OlT6wMh33FSXK/mxikHK136fV2shwTKQVII
+rJBvXpj8nACUmi7scKuTWGeUoXa+dwTZVVe+b+L2U1ZM7+h/neTJiXn7u99PFUwu
+xVJtxaV37m3aXxtCsPnbBg==
+-----END DSA PRIVATE KEY-----"""
+
+ def testImportKey8(self):
+ for pem in (self.pem_private_encrypted, tostr(self.pem_private_encrypted)):
+ key_obj = DSA.importKey(pem, "PWDTEST")
+ self.assertTrue(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+ self.assertEqual(self.x, key_obj.x)
+
+ def testExportKey8(self):
+ tup = (self.y, self.g, self.p, self.q, self.x)
+ key = DSA.construct(tup)
+ encoded = key.export_key('PEM', pkcs8=False, passphrase="PWDTEST")
+ key = DSA.importKey(encoded, "PWDTEST")
+ self.assertEqual(self.y, key.y)
+ self.assertEqual(self.p, key.p)
+ self.assertEqual(self.q, key.q)
+ self.assertEqual(self.g, key.g)
+ self.assertEqual(self.x, key.x)
+
+ # 9. Encrypted PKCS8
+ # pbeWithMD5AndDES-CBC
+ pem_pkcs8_encrypted="""\
+-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBcTAbBgkqhkiG9w0BBQMwDgQI0GC3BJ/jSw8CAggABIIBUHc1cXZpExIE9tC7
+7ryiW+5ihtF2Ekurq3e408GYSAu5smJjN2bvQXmzRFBz8W38K8eMf1sbWroZ4+zn
+kZSbb9nSm5kAa8lR2+oF2k+WRswMR/PTC3f/D9STO2X0QxdrzKgIHEcSGSHp5jTx
+aVvbkCDHo9vhBTl6S3ogZ48As/MEro76+9igUwJ1jNhIQZPJ7e20QH5qDpQFFJN4
+CKl2ENSEuwGiqBszItFy4dqH0g63ZGZV/xt9wSO9Rd7SK/EbA/dklOxBa5Y/VItM
+gnIhs9XDMoGYyn6F023EicNJm6g/bVQk81BTTma4tm+12TKGdYm+QkeZvCOMZylr
+Wv67cKwO3cAXt5C3QXMDgYR64XvuaT5h7C0igMp2afSXJlnbHEbFxQVJlv83T4FM
+eZ4k+NQDbEL8GiHmFxzDWQAuPPZKJWEEEV2p/To+WOh+kSDHQw==
+-----END ENCRYPTED PRIVATE KEY-----"""
+
+ def testImportKey9(self):
+ for pem in (self.pem_pkcs8_encrypted, tostr(self.pem_pkcs8_encrypted)):
+ key_obj = DSA.importKey(pem, "PWDTEST")
+ self.assertTrue(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+ self.assertEqual(self.x, key_obj.x)
+
+ # 10. Encrypted PKCS8
+ # pkcs5PBES2 /
+ # pkcs5PBKDF2 (rounds=1000, salt=D725BF1B6B8239F4) /
+ # des-EDE3-CBC (iv=27A1C66C42AFEECE)
+ #
+ der_pkcs8_encrypted=\
+ '30820196304006092a864886f70d01050d3033301b06092a864886f70d01050c'+\
+ '300e0408d725bf1b6b8239f4020203e8301406082a864886f70d0307040827a1'+\
+ 'c66c42afeece048201505cacfde7bf8edabb3e0d387950dc872662ea7e9b1ed4'+\
+ '400d2e7e6186284b64668d8d0328c33a9d9397e6f03df7cb68268b0a06b4e22f'+\
+ '7d132821449ecf998a8b696dbc6dd2b19e66d7eb2edfeb4153c1771d49702395'+\
+ '4f36072868b5fcccf93413a5ac4b2eb47d4b3f681c6bd67ae363ed776f45ae47'+\
+ '174a00098a7c930a50f820b227ddf50f9742d8e950d02586ff2dac0e3c372248'+\
+ 'e5f9b6a7a02f4004f20c87913e0f7b52bccc209b95d478256a890b31d4c9adec'+\
+ '21a4d157a179a93a3dad06f94f3ce486b46dfa7fc15fd852dd7680bbb2f17478'+\
+ '7e71bd8dbaf81eca7518d76c1d26256e95424864ba45ca5d47d7c5a421be02fa'+\
+ 'b94ab01e18593f66cf9094eb5c94b9ecf3aa08b854a195cf87612fbe5e96c426'+\
+ '2b0d573e52dc71ba3f5e468c601e816c49b7d32c698b22175e89aaef0c443770'+\
+ '5ef2f88a116d99d8e2869a4fd09a771b84b49e4ccb79aadcb1c9'
+
+ def testImportKey10(self):
+ key_obj = DSA.importKey(self.der_pkcs8_encrypted, "PWDTEST")
+ self.assertTrue(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+ self.assertEqual(self.x, key_obj.x)
+
+ def testExportKey10(self):
+ tup = (self.y, self.g, self.p, self.q, self.x)
+ key = DSA.construct(tup)
+ randfunc = BytesIO(unhexlify(b("27A1C66C42AFEECE") + b("D725BF1B6B8239F4"))).read
+ encoded = key.export_key('DER', pkcs8=True, passphrase="PWDTEST", randfunc=randfunc)
+ self.assertEqual(self.der_pkcs8_encrypted, encoded)
+
+ # ----
+
+ def testImportError1(self):
+ self.assertRaises(ValueError, DSA.importKey, self.der_pkcs8_encrypted, "wrongpwd")
+
+ def testExportError2(self):
+ tup = (self.y, self.g, self.p, self.q, self.x)
+ key = DSA.construct(tup)
+ self.assertRaises(ValueError, key.export_key, 'DER', pkcs8=False, passphrase="PWDTEST")
+
+ def test_import_key(self):
+ """Verify importKey is an alias to import_key"""
+
+ key_obj = DSA.import_key(self.der_public)
+ self.assertFalse(key_obj.has_private())
+ self.assertEqual(self.y, key_obj.y)
+ self.assertEqual(self.p, key_obj.p)
+ self.assertEqual(self.q, key_obj.q)
+ self.assertEqual(self.g, key_obj.g)
+
+ def test_exportKey(self):
+ tup = (self.y, self.g, self.p, self.q, self.x)
+ key = DSA.construct(tup)
+ self.assertEqual(key.exportKey(), key.export_key())
+
+
+ def test_import_empty(self):
+ self.assertRaises(ValueError, DSA.import_key, b'')
+
+
+class ImportKeyFromX509Cert(unittest.TestCase):
+
+ def test_x509v1(self):
+
+ # Sample V1 certificate with a 1024 bit DSA key
+ x509_v1_cert = """
+-----BEGIN CERTIFICATE-----
+MIIDUjCCArsCAQIwDQYJKoZIhvcNAQEFBQAwfjENMAsGA1UEChMEQWNtZTELMAkG
+A1UECxMCUkQxHDAaBgkqhkiG9w0BCQEWDXNwYW1AYWNtZS5vcmcxEzARBgNVBAcT
+Ck1ldHJvcG9saXMxETAPBgNVBAgTCE5ldyBZb3JrMQswCQYDVQQGEwJVUzENMAsG
+A1UEAxMEdGVzdDAeFw0xNDA3MTEyMDM4NDNaFw0xNzA0MDYyMDM4NDNaME0xCzAJ
+BgNVBAYTAlVTMREwDwYDVQQIEwhOZXcgWW9yazENMAsGA1UEChMEQWNtZTELMAkG
+A1UECxMCUkQxDzANBgNVBAMTBnBvbGFuZDCCAbYwggErBgcqhkjOOAQBMIIBHgKB
+gQDOrN4Ox4+t3T6wKeHfhzArhcrNEFMQ4Ss+4PIKyimDy9Bn64WPkL1B/9dvYIga
+23GLu6tVJmXo6EdJnVOHEMhr99EeOwuDWWeP7Awq7RSlKEejokr4BEzMTW/tExSD
+cO6/GI7xzh0eTH+VTTPDfyrJMYCkh0rJAfCP+5xrmPNetwIVALtXYOV1yoRrzJ2Q
+M5uEjidH6GiZAoGAfUqA1SAm5g5U68SILMVX9l5rq0OpB0waBMpJQ31/R/yXNDqo
+c3gGWZTOJFU4IzwNpGhrGNADUByz/lc1SAOAdEJIr0JVrhbGewQjB4pWqoLGbBKz
+RoavTNDc/zD7SYa12evWDHADwvlXoeQg+lWop1zS8OqaDC7aLGKpWN3/m8kDgYQA
+AoGAKoirPAfcp1rbbl4y2FFAIktfW8f4+T7d2iKSg73aiVfujhNOt1Zz1lfC0NI2
+eonLWO3tAM4XGKf1TLjb5UXngGn40okPsaA81YE6ZIKm20ywjlOY3QkAEdMaLVY3
+9PJvM8RGB9m7pLKxyHfGMfF40MVN4222zKeGp7xhM0CNiCUwDQYJKoZIhvcNAQEF
+BQADgYEAfbNZfpYa2KlALEM1FZnwvQDvJHntHz8LdeJ4WM7CXDlKi67wY2HKM30w
+s2xej75imkVOFd1kF2d0A8sjfriXLVIt1Hwq9ANZomhu4Edx0xpH8tqdh/bDtnM2
+TmduZNY9OWkb07h0CtWD6Zt8fhRllVsSSrlWd/2or7FXNC5weFQ=
+-----END CERTIFICATE-----
+ """.strip()
+
+ # DSA public key as dumped by openssl
+ y_str = """
+2a:88:ab:3c:07:dc:a7:5a:db:6e:5e:32:d8:51:40:
+22:4b:5f:5b:c7:f8:f9:3e:dd:da:22:92:83:bd:da:
+89:57:ee:8e:13:4e:b7:56:73:d6:57:c2:d0:d2:36:
+7a:89:cb:58:ed:ed:00:ce:17:18:a7:f5:4c:b8:db:
+e5:45:e7:80:69:f8:d2:89:0f:b1:a0:3c:d5:81:3a:
+64:82:a6:db:4c:b0:8e:53:98:dd:09:00:11:d3:1a:
+2d:56:37:f4:f2:6f:33:c4:46:07:d9:bb:a4:b2:b1:
+c8:77:c6:31:f1:78:d0:c5:4d:e3:6d:b6:cc:a7:86:
+a7:bc:61:33:40:8d:88:25
+ """
+ p_str = """
+00:ce:ac:de:0e:c7:8f:ad:dd:3e:b0:29:e1:df:87:
+30:2b:85:ca:cd:10:53:10:e1:2b:3e:e0:f2:0a:ca:
+29:83:cb:d0:67:eb:85:8f:90:bd:41:ff:d7:6f:60:
+88:1a:db:71:8b:bb:ab:55:26:65:e8:e8:47:49:9d:
+53:87:10:c8:6b:f7:d1:1e:3b:0b:83:59:67:8f:ec:
+0c:2a:ed:14:a5:28:47:a3:a2:4a:f8:04:4c:cc:4d:
+6f:ed:13:14:83:70:ee:bf:18:8e:f1:ce:1d:1e:4c:
+7f:95:4d:33:c3:7f:2a:c9:31:80:a4:87:4a:c9:01:
+f0:8f:fb:9c:6b:98:f3:5e:b7
+ """
+ q_str = """
+00:bb:57:60:e5:75:ca:84:6b:cc:9d:90:33:9b:84:
+8e:27:47:e8:68:99
+ """
+ g_str = """
+7d:4a:80:d5:20:26:e6:0e:54:eb:c4:88:2c:c5:57:
+f6:5e:6b:ab:43:a9:07:4c:1a:04:ca:49:43:7d:7f:
+47:fc:97:34:3a:a8:73:78:06:59:94:ce:24:55:38:
+23:3c:0d:a4:68:6b:18:d0:03:50:1c:b3:fe:57:35:
+48:03:80:74:42:48:af:42:55:ae:16:c6:7b:04:23:
+07:8a:56:aa:82:c6:6c:12:b3:46:86:af:4c:d0:dc:
+ff:30:fb:49:86:b5:d9:eb:d6:0c:70:03:c2:f9:57:
+a1:e4:20:fa:55:a8:a7:5c:d2:f0:ea:9a:0c:2e:da:
+2c:62:a9:58:dd:ff:9b:c9
+ """
+
+ key = DSA.importKey(x509_v1_cert)
+ for comp_name in ('y', 'p', 'q', 'g'):
+ comp_str = locals()[comp_name + "_str"]
+ comp = int(re.sub("[^0-9a-f]", "", comp_str), 16)
+ self.assertEqual(getattr(key, comp_name), comp)
+ self.assertFalse(key.has_private())
+
+ def test_x509v3(self):
+
+ # Sample V3 certificate with a 1024 bit DSA key
+ x509_v3_cert = """
+-----BEGIN CERTIFICATE-----
+MIIFhjCCA26gAwIBAgIBAzANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCTUQxEjAQBgNVBAcMCUJhbHRpbW9yZTEQMA4GA1UEAwwHVGVzdCBD
+QTEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTAeFw0xNDA3MTMyMDUz
+MjBaFw0xNzA0MDgyMDUzMjBaMEAxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNRDES
+MBAGA1UEBwwJQmFsdGltb3JlMRAwDgYDVQQDDAdhdXN0cmlhMIIBtjCCASsGByqG
+SM44BAEwggEeAoGBALfd8gyEpVPA0ZI69Kp3nyJcu5N0ZZ3K1K9hleQLNqKEcZOh
+7a/C2J1TPdmHTLJ0rAwBZ1nWxnARSgRphziGDFspKCYQwYcSMz8KoFgvXbXpuchy
+oFACiQ2LqZnc5MakuLQtLcQciSYGYj3zmZdYMoa904F1aDWr+DxQI6DVC3/bAhUA
+hqXMCJ6fQK3G2O9S3/CC/yVZXCsCgYBRXROl3R2khX7l10LQjDEgo3B1IzjXU/jP
+McMBl6XO+nBJXxr/scbq8Ajiv7LTnGpSjgryHtvfj887kfvo8QbSS3kp3vq5uSqI
+ui7E7r3jguWaLj616AG1HWOctXJUjqsiabZwsp2h09gHTzmHEXBOmiARu8xFxKAH
+xsuo7onAbwOBhAACgYBylWjWSnKHE8mHx1A5m/0GQx6xnhWIe3+MJAnEhRGxA2J4
+SCsfWU0OwglIQToh1z5uUU9oDi9cYgNPBevOFRnDhc2yaJY6VAYnI+D+6J5IU6Yd
+0iaG/iSc4sV4bFr0axcPpse3SN0XaQxiKeSFBfFnoMqL+dd9Gb3QPZSllBcVD6OB
+1TCB0jAdBgNVHQ4EFgQUx5wN0Puotv388M9Tp/fsPbZpzAUwHwYDVR0jBBgwFoAU
+a0hkif3RMaraiWtsOOZZlLu9wJwwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwSgYD
+VR0RBEMwQYILZXhhbXBsZS5jb22CD3d3dy5leGFtcGxlLmNvbYIQbWFpbC5leGFt
+cGxlLmNvbYIPZnRwLmV4YW1wbGUuY29tMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM
+IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTANBgkqhkiG9w0BAQsFAAOCAgEAyWf1TiJI
+aNEIA9o/PG8/JiGASTS2/HBVTJbkq03k6NkJVk/GxC1DPziTUJ+CdWlHWcAi1EOW
+Ach3QxNDRrVfCOfCMDgElIO1094/reJgdFYG00LRi8QkRJuxANV7YS4tLudhyHJC
+kR2lhdMNmEuzWK+s2y+5cLrdm7qdvdENQCcV67uvGPx4sc+EaE7x13SczKjWBtbo
+QCs6JTOW+EkPRl4Zo27K4OIZ43/J+GxvwU9QUVH3wPVdbbLNw+QeTFBYMTEcxyc4
+kv50HPBFaithziXBFyvdIs19FjkFzu0Uz/e0zb1+vMzQlJMD94HVOrMnIj5Sb2cL
+KKdYXS4uhxFJmdV091Xur5JkYYwEzuaGav7J3zOzYutrIGTgDluLCvA+VQkRcTsy
+jZ065SkY/v+38QHp+cmm8WRluupJTs8wYzVp6Fu0iFaaK7ztFmaZmHpiPIfDFjva
+aCIgzzT5NweJd/b71A2SyzHXJ14zBXsr1PMylMp2TpHIidhuuNuQL6I0HaollB4M
+Z3FsVBMhVDw4Z76qnFPr8mZE2tar33hSlJI/3pS/bBiukuBk8U7VB0X8OqaUnP3C
+7b2Z4G8GtqDVcKGMzkvMjT4n9rKd/Le+qHSsQOGO9W/0LB7UDAZSwUsfAPnoBgdS
+5t9tIomLCOstByXi+gGZue1TcdCa3Ph4kO0=
+-----END CERTIFICATE-----
+ """.strip()
+
+ # DSA public key as dumped by openssl
+ y_str = """
+72:95:68:d6:4a:72:87:13:c9:87:c7:50:39:9b:fd:
+06:43:1e:b1:9e:15:88:7b:7f:8c:24:09:c4:85:11:
+b1:03:62:78:48:2b:1f:59:4d:0e:c2:09:48:41:3a:
+21:d7:3e:6e:51:4f:68:0e:2f:5c:62:03:4f:05:eb:
+ce:15:19:c3:85:cd:b2:68:96:3a:54:06:27:23:e0:
+fe:e8:9e:48:53:a6:1d:d2:26:86:fe:24:9c:e2:c5:
+78:6c:5a:f4:6b:17:0f:a6:c7:b7:48:dd:17:69:0c:
+62:29:e4:85:05:f1:67:a0:ca:8b:f9:d7:7d:19:bd:
+d0:3d:94:a5:94:17:15:0f
+ """
+ p_str = """
+00:b7:dd:f2:0c:84:a5:53:c0:d1:92:3a:f4:aa:77:
+9f:22:5c:bb:93:74:65:9d:ca:d4:af:61:95:e4:0b:
+36:a2:84:71:93:a1:ed:af:c2:d8:9d:53:3d:d9:87:
+4c:b2:74:ac:0c:01:67:59:d6:c6:70:11:4a:04:69:
+87:38:86:0c:5b:29:28:26:10:c1:87:12:33:3f:0a:
+a0:58:2f:5d:b5:e9:b9:c8:72:a0:50:02:89:0d:8b:
+a9:99:dc:e4:c6:a4:b8:b4:2d:2d:c4:1c:89:26:06:
+62:3d:f3:99:97:58:32:86:bd:d3:81:75:68:35:ab:
+f8:3c:50:23:a0:d5:0b:7f:db
+ """
+ q_str = """
+00:86:a5:cc:08:9e:9f:40:ad:c6:d8:ef:52:df:f0:
+82:ff:25:59:5c:2b
+ """
+ g_str = """
+51:5d:13:a5:dd:1d:a4:85:7e:e5:d7:42:d0:8c:31:
+20:a3:70:75:23:38:d7:53:f8:cf:31:c3:01:97:a5:
+ce:fa:70:49:5f:1a:ff:b1:c6:ea:f0:08:e2:bf:b2:
+d3:9c:6a:52:8e:0a:f2:1e:db:df:8f:cf:3b:91:fb:
+e8:f1:06:d2:4b:79:29:de:fa:b9:b9:2a:88:ba:2e:
+c4:ee:bd:e3:82:e5:9a:2e:3e:b5:e8:01:b5:1d:63:
+9c:b5:72:54:8e:ab:22:69:b6:70:b2:9d:a1:d3:d8:
+07:4f:39:87:11:70:4e:9a:20:11:bb:cc:45:c4:a0:
+07:c6:cb:a8:ee:89:c0:6f
+ """
+
+ key = DSA.importKey(x509_v3_cert)
+ for comp_name in ('y', 'p', 'q', 'g'):
+ comp_str = locals()[comp_name + "_str"]
+ comp = int(re.sub("[^0-9a-f]", "", comp_str), 16)
+ self.assertEqual(getattr(key, comp_name), comp)
+ self.assertFalse(key.has_private())
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(ImportKeyTests)
+ tests += list_test_cases(ImportKeyFromX509Cert)
+ return tests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_ECC.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_ECC.py
new file mode 100644
index 00000000..16f21622
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_ECC.py
@@ -0,0 +1,2782 @@
+# ===================================================================
+#
+# Copyright (c) 2015, Legrandin
+# 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 os
+import errno
+import warnings
+import unittest
+from binascii import unhexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Util.py3compat import bord, tostr, FileNotFoundError
+from Crypto.Util.asn1 import DerSequence, DerBitString
+from Crypto.Util.number import bytes_to_long
+from Crypto.Hash import SHAKE128
+
+from Crypto.PublicKey import ECC
+
+from Crypto.PublicKey.ECC import _import_rfc5915_der
+
+try:
+ import pycryptodome_test_vectors # type: ignore
+ test_vectors_available = True
+except ImportError:
+ test_vectors_available = False
+
+
+class MissingTestVectorException(ValueError):
+ pass
+
+
+def load_file(file_name, mode="rb"):
+ results = None
+
+ try:
+ if not test_vectors_available:
+ raise FileNotFoundError(errno.ENOENT,
+ os.strerror(errno.ENOENT),
+ file_name)
+
+ dir_comps = ("PublicKey", "ECC")
+ init_dir = os.path.dirname(pycryptodome_test_vectors.__file__)
+ full_file_name = os.path.join(os.path.join(init_dir, *dir_comps), file_name)
+ with open(full_file_name, mode) as file_in:
+ results = file_in.read()
+
+ except FileNotFoundError:
+ warnings.warn("Skipping extended tests for ECC",
+ UserWarning,
+ stacklevel=2)
+
+ if results is None:
+ raise MissingTestVectorException("Missing %s" % file_name)
+
+ return results
+
+
+def compact(lines):
+ ext = b"".join(lines)
+ return unhexlify(tostr(ext).replace(" ", "").replace(":", ""))
+
+
+def create_ref_keys_p192():
+ key_len = 24
+ key_lines = load_file("ecc_p192.txt").splitlines()
+ private_key_d = bytes_to_long(compact(key_lines[2:4]))
+ public_key_xy = compact(key_lines[5:9])
+ assert bord(public_key_xy[0]) == 4 # Uncompressed
+ public_key_x = bytes_to_long(public_key_xy[1:key_len+1])
+ public_key_y = bytes_to_long(public_key_xy[key_len+1:])
+
+ return (ECC.construct(curve="P-192", d=private_key_d),
+ ECC.construct(curve="P-192", point_x=public_key_x, point_y=public_key_y))
+
+
+def create_ref_keys_p224():
+ key_len = 28
+ key_lines = load_file("ecc_p224.txt").splitlines()
+ private_key_d = bytes_to_long(compact(key_lines[2:4]))
+ public_key_xy = compact(key_lines[5:9])
+ assert bord(public_key_xy[0]) == 4 # Uncompressed
+ public_key_x = bytes_to_long(public_key_xy[1:key_len+1])
+ public_key_y = bytes_to_long(public_key_xy[key_len+1:])
+
+ return (ECC.construct(curve="P-224", d=private_key_d),
+ ECC.construct(curve="P-224", point_x=public_key_x, point_y=public_key_y))
+
+
+def create_ref_keys_p256():
+ key_len = 32
+ key_lines = load_file("ecc_p256.txt").splitlines()
+ private_key_d = bytes_to_long(compact(key_lines[2:5]))
+ public_key_xy = compact(key_lines[6:11])
+ assert bord(public_key_xy[0]) == 4 # Uncompressed
+ public_key_x = bytes_to_long(public_key_xy[1:key_len+1])
+ public_key_y = bytes_to_long(public_key_xy[key_len+1:])
+
+ return (ECC.construct(curve="P-256", d=private_key_d),
+ ECC.construct(curve="P-256", point_x=public_key_x, point_y=public_key_y))
+
+
+def create_ref_keys_p384():
+ key_len = 48
+ key_lines = load_file("ecc_p384.txt").splitlines()
+ private_key_d = bytes_to_long(compact(key_lines[2:6]))
+ public_key_xy = compact(key_lines[7:14])
+ assert bord(public_key_xy[0]) == 4 # Uncompressed
+ public_key_x = bytes_to_long(public_key_xy[1:key_len+1])
+ public_key_y = bytes_to_long(public_key_xy[key_len+1:])
+
+ return (ECC.construct(curve="P-384", d=private_key_d),
+ ECC.construct(curve="P-384", point_x=public_key_x, point_y=public_key_y))
+
+
+def create_ref_keys_p521():
+ key_len = 66
+ key_lines = load_file("ecc_p521.txt").splitlines()
+ private_key_d = bytes_to_long(compact(key_lines[2:7]))
+ public_key_xy = compact(key_lines[8:17])
+ assert bord(public_key_xy[0]) == 4 # Uncompressed
+ public_key_x = bytes_to_long(public_key_xy[1:key_len+1])
+ public_key_y = bytes_to_long(public_key_xy[key_len+1:])
+
+ return (ECC.construct(curve="P-521", d=private_key_d),
+ ECC.construct(curve="P-521", point_x=public_key_x, point_y=public_key_y))
+
+
+def create_ref_keys_ed25519():
+ key_lines = load_file("ecc_ed25519.txt").splitlines()
+ seed = compact(key_lines[5:8])
+ key = ECC.construct(curve="Ed25519", seed=seed)
+ return (key, key.public_key())
+
+
+def create_ref_keys_ed448():
+ key_lines = load_file("ecc_ed448.txt").splitlines()
+ seed = compact(key_lines[6:10])
+ key = ECC.construct(curve="Ed448", seed=seed)
+ return (key, key.public_key())
+
+
+# Create reference key pair
+# ref_private, ref_public = create_ref_keys_p521()
+
+def get_fixed_prng():
+ return SHAKE128.new().update(b"SEED").read
+
+
+def extract_bitstring_from_spki(data):
+ seq = DerSequence()
+ seq.decode(data)
+ bs = DerBitString()
+ bs.decode(seq[1])
+ return bs.value
+
+
+class TestImport(unittest.TestCase):
+
+ def test_empty(self):
+ self.assertRaises(ValueError, ECC.import_key, b"")
+
+ def test_mismatch(self):
+ # The private key does not match the public key
+ mismatch = """-----BEGIN PRIVATE KEY-----
+MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJChZANiAAQarFRaqflo
+I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng
+o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXk=
+-----END PRIVATE KEY-----"""
+ self.assertRaises(ValueError, ECC.import_key, mismatch)
+
+ def test_import_private_rfc5915_none(self):
+ # ECPrivateKey with a P256 private key, without [0] and [1]
+ data_hex = "302502010104205c4e4320ef260f91ed9fc597aee98c8236b60e0ced692cc7a057d5e45798a052"
+ key = _import_rfc5915_der(unhexlify(data_hex), None, "1.2.840.10045.3.1.7")
+ self.assertEqual(key.d, 0x5c4e4320ef260f91ed9fc597aee98c8236b60e0ced692cc7a057d5e45798a052)
+
+ def test_import_private_rfc5915_only_0(self):
+ # ECPrivateKey with a P256 private key, with [0] only
+ data_hex = "303102010104205c4e4320ef260f91ed9fc597aee98c8236b60e0ced692cc7a057d5e45798a052a00a06082a8648ce3d030107"
+ key = _import_rfc5915_der(unhexlify(data_hex), None)
+ self.assertEqual(key.d, 0x5c4e4320ef260f91ed9fc597aee98c8236b60e0ced692cc7a057d5e45798a052)
+
+ def test_import_private_rfc5915_only_1(self):
+ # ECPrivateKey with a P256 private key, with [1] only
+ data_hex = "306b02010104205c4e4320ef260f91ed9fc597aee98c8236b60e0ced692cc7a057d5e45798a052a14403420004a40ad59a2050ebe92479bd5fb16bb2e45b6465eb3cb2b1effe423fabe6cb7424db8219ef0bab80acf26fd70595b61fe4760d33eed80dd03d2fd0dfb27b8ce75c"
+ key = _import_rfc5915_der(unhexlify(data_hex), None, "1.2.840.10045.3.1.7")
+ self.assertEqual(key.d, 0x5c4e4320ef260f91ed9fc597aee98c8236b60e0ced692cc7a057d5e45798a052)
+
+class TestImport_P192(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestImport_P192, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_p192()
+
+ def test_import_public_der(self):
+ key_file = load_file("ecc_p192_public.der")
+
+ key = ECC._import_subjectPublicKeyInfo(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_sec1_uncompressed(self):
+ key_file = load_file("ecc_p192_public.der")
+ value = extract_bitstring_from_spki(key_file)
+ key = ECC.import_key(key_file, curve_name='P192')
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_sec1_compressed(self):
+ key_file = load_file("ecc_p192_public_compressed.der")
+ value = extract_bitstring_from_spki(key_file)
+ key = ECC.import_key(key_file, curve_name='P192')
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_rfc5915_der(self):
+ key_file = load_file("ecc_p192_private.der")
+
+ key = ECC._import_rfc5915_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_clear(self):
+ key_file = load_file("ecc_p192_private_p8_clear.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_in_pem_clear(self):
+ key_file = load_file("ecc_p192_private_p8_clear.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_1(self):
+ key_file = load_file("ecc_p192_private_p8.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_2(self):
+ key_file = load_file("ecc_p192_private_p8.pem")
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_der(self):
+ key_file = load_file("ecc_p192_x509.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_public_pem(self):
+ key_file = load_file("ecc_p192_public.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_private_pem(self):
+ key_file = load_file("ecc_p192_private.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pem_encrypted(self):
+ for algo in "des3", "aes128", "aes192", "aes256", "aes256_gcm":
+ key_file = load_file("ecc_p192_private_enc_%s.pem" % algo)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(tostr(key_file), b"secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_pem(self):
+ key_file = load_file("ecc_p192_x509.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+
+class TestImport_P224(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestImport_P224, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_p224()
+
+ def test_import_public_der(self):
+ key_file = load_file("ecc_p224_public.der")
+
+ key = ECC._import_subjectPublicKeyInfo(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_sec1_uncompressed(self):
+ key_file = load_file("ecc_p224_public.der")
+ value = extract_bitstring_from_spki(key_file)
+ key = ECC.import_key(key_file, curve_name='P224')
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_sec1_compressed(self):
+ key_file = load_file("ecc_p224_public_compressed.der")
+ value = extract_bitstring_from_spki(key_file)
+ key = ECC.import_key(key_file, curve_name='P224')
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_rfc5915_der(self):
+ key_file = load_file("ecc_p224_private.der")
+
+ key = ECC._import_rfc5915_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_clear(self):
+ key_file = load_file("ecc_p224_private_p8_clear.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_in_pem_clear(self):
+ key_file = load_file("ecc_p224_private_p8_clear.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_1(self):
+ key_file = load_file("ecc_p224_private_p8.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_2(self):
+ key_file = load_file("ecc_p224_private_p8.pem")
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_3(self):
+ key_file = load_file("ecc_p224_private_p8_2.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_der(self):
+ key_file = load_file("ecc_p224_x509.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_public_pem(self):
+ key_file = load_file("ecc_p224_public.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_private_pem(self):
+ key_file = load_file("ecc_p224_private.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pem_encrypted(self):
+ for algo in "des3", "aes128", "aes192", "aes256", "aes256_gcm":
+ key_file = load_file("ecc_p224_private_enc_%s.pem" % algo)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(tostr(key_file), b"secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_pem(self):
+ key_file = load_file("ecc_p224_x509.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+
+class TestImport_P256(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestImport_P256, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_p256()
+
+ def test_import_public_der(self):
+ key_file = load_file("ecc_p256_public.der")
+
+ key = ECC._import_subjectPublicKeyInfo(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_sec1_uncompressed(self):
+ key_file = load_file("ecc_p256_public.der")
+ value = extract_bitstring_from_spki(key_file)
+ key = ECC.import_key(key_file, curve_name='P256')
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_sec1_compressed(self):
+ key_file = load_file("ecc_p256_public_compressed.der")
+ value = extract_bitstring_from_spki(key_file)
+ key = ECC.import_key(key_file, curve_name='P256')
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_rfc5915_der(self):
+ key_file = load_file("ecc_p256_private.der")
+
+ key = ECC._import_rfc5915_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_clear(self):
+ key_file = load_file("ecc_p256_private_p8_clear.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_in_pem_clear(self):
+ key_file = load_file("ecc_p256_private_p8_clear.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_1(self):
+ key_file = load_file("ecc_p256_private_p8.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_2(self):
+ key_file = load_file("ecc_p256_private_p8.pem")
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_3(self):
+ key_file = load_file("ecc_p256_private_p8_2.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_der(self):
+ key_file = load_file("ecc_p256_x509.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_public_pem(self):
+ key_file = load_file("ecc_p256_public.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_private_pem(self):
+ key_file = load_file("ecc_p256_private.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pem_with_ecparams(self):
+ key_file = load_file("ecc_p256_private_ecparams.pem")
+ key = ECC.import_key(key_file)
+ # We just check if the import succeeds
+
+ def test_import_private_pem_encrypted(self):
+ for algo in "des3", "aes128", "aes192", "aes256", "aes256_gcm":
+ key_file = load_file("ecc_p256_private_enc_%s.pem" % algo)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(tostr(key_file), b"secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_pem(self):
+ key_file = load_file("ecc_p256_x509.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_openssh_public(self):
+ key_file = load_file("ecc_p256_public_openssh.txt")
+
+ key = ECC._import_openssh_public(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_openssh_private_clear(self):
+ key_file = load_file("ecc_p256_private_openssh.pem")
+ key_file_old = load_file("ecc_p256_private_openssh_old.pem")
+
+ key = ECC.import_key(key_file)
+ key_old = ECC.import_key(key_file_old)
+ self.assertEqual(key, key_old)
+
+ def test_import_openssh_private_password(self):
+ key_file = load_file("ecc_p256_private_openssh_pwd.pem")
+ key_file_old = load_file("ecc_p256_private_openssh_pwd_old.pem")
+
+ key = ECC.import_key(key_file, b"password")
+ key_old = ECC.import_key(key_file_old)
+ self.assertEqual(key, key_old)
+
+
+class TestImport_P384(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestImport_P384, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_p384()
+
+ def test_import_public_der(self):
+ key_file = load_file("ecc_p384_public.der")
+
+ key = ECC._import_subjectPublicKeyInfo(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_sec1_uncompressed(self):
+ key_file = load_file("ecc_p384_public.der")
+ value = extract_bitstring_from_spki(key_file)
+ key = ECC.import_key(key_file, curve_name='P384')
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_sec1_compressed(self):
+ key_file = load_file("ecc_p384_public_compressed.der")
+ value = extract_bitstring_from_spki(key_file)
+ key = ECC.import_key(key_file, curve_name='P384')
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_rfc5915_der(self):
+ key_file = load_file("ecc_p384_private.der")
+
+ key = ECC._import_rfc5915_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_clear(self):
+ key_file = load_file("ecc_p384_private_p8_clear.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_in_pem_clear(self):
+ key_file = load_file("ecc_p384_private_p8_clear.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_1(self):
+ key_file = load_file("ecc_p384_private_p8.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_2(self):
+ key_file = load_file("ecc_p384_private_p8.pem")
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_3(self):
+ key_file = load_file("ecc_p384_private_p8_2.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_der(self):
+ key_file = load_file("ecc_p384_x509.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_public_pem(self):
+ key_file = load_file("ecc_p384_public.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_private_pem(self):
+ key_file = load_file("ecc_p384_private.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pem_encrypted(self):
+ for algo in "des3", "aes128", "aes192", "aes256", "aes256_gcm":
+ key_file = load_file("ecc_p384_private_enc_%s.pem" % algo)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(tostr(key_file), b"secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_pem(self):
+ key_file = load_file("ecc_p384_x509.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_openssh_public(self):
+ key_file = load_file("ecc_p384_public_openssh.txt")
+
+ key = ECC._import_openssh_public(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_openssh_private_clear(self):
+ key_file = load_file("ecc_p384_private_openssh.pem")
+ key_file_old = load_file("ecc_p384_private_openssh_old.pem")
+
+ key = ECC.import_key(key_file)
+ key_old = ECC.import_key(key_file_old)
+ self.assertEqual(key, key_old)
+
+ def test_import_openssh_private_password(self):
+ key_file = load_file("ecc_p384_private_openssh_pwd.pem")
+ key_file_old = load_file("ecc_p384_private_openssh_pwd_old.pem")
+
+ key = ECC.import_key(key_file, b"password")
+ key_old = ECC.import_key(key_file_old)
+ self.assertEqual(key, key_old)
+
+
+class TestImport_P521(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestImport_P521, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_p521()
+
+ def test_import_public_der(self):
+ key_file = load_file("ecc_p521_public.der")
+
+ key = ECC._import_subjectPublicKeyInfo(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_sec1_uncompressed(self):
+ key_file = load_file("ecc_p521_public.der")
+ value = extract_bitstring_from_spki(key_file)
+ key = ECC.import_key(key_file, curve_name='P521')
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_sec1_compressed(self):
+ key_file = load_file("ecc_p521_public_compressed.der")
+ value = extract_bitstring_from_spki(key_file)
+ key = ECC.import_key(key_file, curve_name='P521')
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_rfc5915_der(self):
+ key_file = load_file("ecc_p521_private.der")
+
+ key = ECC._import_rfc5915_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_clear(self):
+ key_file = load_file("ecc_p521_private_p8_clear.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_in_pem_clear(self):
+ key_file = load_file("ecc_p521_private_p8_clear.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_1(self):
+ key_file = load_file("ecc_p521_private_p8.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_2(self):
+ key_file = load_file("ecc_p521_private_p8.pem")
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_3(self):
+ key_file = load_file("ecc_p521_private_p8_2.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_der(self):
+ key_file = load_file("ecc_p521_x509.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_public_pem(self):
+ key_file = load_file("ecc_p521_public.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_private_pem(self):
+ key_file = load_file("ecc_p521_private.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pem_encrypted(self):
+ for algo in "des3", "aes128", "aes192", "aes256", "aes256_gcm":
+ key_file = load_file("ecc_p521_private_enc_%s.pem" % algo)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(tostr(key_file), b"secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_pem(self):
+ key_file = load_file("ecc_p521_x509.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_openssh_public(self):
+ key_file = load_file("ecc_p521_public_openssh.txt")
+
+ key = ECC._import_openssh_public(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_openssh_private_clear(self):
+ key_file = load_file("ecc_p521_private_openssh.pem")
+ key_file_old = load_file("ecc_p521_private_openssh_old.pem")
+
+ key = ECC.import_key(key_file)
+ key_old = ECC.import_key(key_file_old)
+ self.assertEqual(key, key_old)
+
+ def test_import_openssh_private_password(self):
+ key_file = load_file("ecc_p521_private_openssh_pwd.pem")
+ key_file_old = load_file("ecc_p521_private_openssh_pwd_old.pem")
+
+ key = ECC.import_key(key_file, b"password")
+ key_old = ECC.import_key(key_file_old)
+ self.assertEqual(key, key_old)
+
+
+class TestExport_P192(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestExport_P192, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_p192()
+
+ def test_export_public_der_uncompressed(self):
+ key_file = load_file("ecc_p192_public.der")
+
+ encoded = self.ref_public._export_subjectPublicKeyInfo(False)
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_der_compressed(self):
+ key_file = load_file("ecc_p192_public.der")
+ pub_key = ECC.import_key(key_file)
+ key_file_compressed = pub_key.export_key(format="DER", compress=True)
+
+ key_file_compressed_ref = load_file("ecc_p192_public_compressed.der")
+ self.assertEqual(key_file_compressed, key_file_compressed_ref)
+
+ def test_export_public_sec1_uncompressed(self):
+ key_file = load_file("ecc_p192_public.der")
+ value = extract_bitstring_from_spki(key_file)
+
+ encoded = self.ref_public.export_key(format="SEC1")
+ self.assertEqual(value, encoded)
+
+ def test_export_public_sec1_compressed(self):
+ key_file = load_file("ecc_p192_public.der")
+ encoded = self.ref_public.export_key(format="SEC1", compress=True)
+
+ key_file_compressed_ref = load_file("ecc_p192_public_compressed.der")
+ value = extract_bitstring_from_spki(key_file_compressed_ref)
+ self.assertEqual(value, encoded)
+
+ def test_export_rfc5915_private_der(self):
+ key_file = load_file("ecc_p192_private.der")
+
+ encoded = self.ref_private._export_rfc5915_private_der()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER", use_pkcs8=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_clear(self):
+ key_file = load_file("ecc_p192_private_p8_clear.der")
+
+ encoded = self.ref_private._export_pkcs8()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_encrypted(self):
+ encoded = self.ref_private._export_pkcs8(passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC._import_pkcs8, encoded, None)
+
+ decoded = ECC._import_pkcs8(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA224AndAES192-CBC",
+ prot_params={'iteration_count':123})
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_public_pem_uncompressed(self):
+ key_file = load_file("ecc_p192_public.pem", "rt").strip()
+
+ encoded = self.ref_private._export_public_pem(False)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_public.export_key(format="PEM")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="PEM", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_pem_compressed(self):
+ key_file = load_file("ecc_p192_public.pem", "rt").strip()
+ pub_key = ECC.import_key(key_file)
+
+ key_file_compressed = pub_key.export_key(format="PEM", compress=True)
+ key_file_compressed_ref = load_file("ecc_p192_public_compressed.pem", "rt").strip()
+
+ self.assertEqual(key_file_compressed, key_file_compressed_ref)
+
+ def test_export_private_pem_clear(self):
+ key_file = load_file("ecc_p192_private.pem", "rt").strip()
+
+ encoded = self.ref_private._export_private_pem(None)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM", use_pkcs8=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pem_encrypted(self):
+ encoded = self.ref_private._export_private_pem(passphrase=b"secret")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "EC PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ use_pkcs8=False)
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_private_pkcs8_and_pem_1(self):
+ # PKCS8 inside PEM with both unencrypted
+ key_file = load_file("ecc_p192_private_p8_clear.pem", "rt").strip()
+
+ encoded = self.ref_private._export_private_clear_pkcs8_in_clear_pem()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM")
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_and_pem_2(self):
+ # PKCS8 inside PEM with PKCS8 encryption
+ encoded = self.ref_private._export_private_encrypted_pkcs8_in_clear_pem("secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "ENCRYPTED PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_prng(self):
+ # Test that password-protected containers use the provided PRNG
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ # ---
+
+ encoded1 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_byte_or_string_passphrase(self):
+ encoded1 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase=b"secret",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_error_params1(self):
+ # Unknown format
+ self.assertRaises(ValueError, self.ref_private.export_key, format="XXX")
+
+ # Missing 'protection' parameter when PKCS#8 is used
+ self.ref_private.export_key(format="PEM", passphrase="secret",
+ use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="secret")
+
+ # DER format but no PKCS#8
+ self.assertRaises(ValueError, self.ref_private.export_key, format="DER",
+ passphrase="secret",
+ use_pkcs8=False,
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # Incorrect parameters for public keys
+ self.assertRaises(ValueError, self.ref_public.export_key, format="DER",
+ use_pkcs8=False)
+
+ # Empty password
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="", use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ def test_compressed_curve(self):
+
+ # Compressed P-192 curve (Y-point is even)
+ pem1 = """-----BEGIN EC PRIVATE KEY-----
+ MF8CAQEEGHvhXmIW95JxZYfd4AUPu9BwknjuvS36aqAKBggqhkjOPQMBAaE0AzIA
+ BLJZCyTu35DQIlqvMlBynn3k1Ig+dWfg/brRhHecxptrbloqFSP8ITw0CwbGF+2X
+ 5g==
+ -----END EC PRIVATE KEY-----"""
+
+ # Compressed P-192 curve (Y-point is odd)
+ pem2 = """-----BEGIN EC PRIVATE KEY-----
+ MF8CAQEEGA3rAotUaWl7d47eX6tz9JmLzOMJwl13XaAKBggqhkjOPQMBAaE0AzIA
+ BG4tHlTBBBGokcWmGm2xubVB0NvPC/Ou5AYwivs+3iCxmEjsymVAj6iiuX2Lxr6g
+ /Q==
+ -----END EC PRIVATE KEY-----"""
+
+ key1 = ECC.import_key(pem1)
+ low16 = int(key1.pointQ.y % 65536)
+ self.assertEqual(low16, 0x97E6)
+
+ key2 = ECC.import_key(pem2)
+ low16 = int(key2.pointQ.y % 65536)
+ self.assertEqual(low16, 0xA0FD)
+
+
+class TestExport_P224(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestExport_P224, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_p224()
+
+ def test_export_public_der_uncompressed(self):
+ key_file = load_file("ecc_p224_public.der")
+
+ encoded = self.ref_public._export_subjectPublicKeyInfo(False)
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_der_compressed(self):
+ key_file = load_file("ecc_p224_public.der")
+ pub_key = ECC.import_key(key_file)
+ key_file_compressed = pub_key.export_key(format="DER", compress=True)
+
+ key_file_compressed_ref = load_file("ecc_p224_public_compressed.der")
+ self.assertEqual(key_file_compressed, key_file_compressed_ref)
+
+ def test_export_public_sec1_uncompressed(self):
+ key_file = load_file("ecc_p224_public.der")
+ value = extract_bitstring_from_spki(key_file)
+
+ encoded = self.ref_public.export_key(format="SEC1")
+ self.assertEqual(value, encoded)
+
+ def test_export_public_sec1_compressed(self):
+ key_file = load_file("ecc_p224_public.der")
+ encoded = self.ref_public.export_key(format="SEC1", compress=True)
+
+ key_file_compressed_ref = load_file("ecc_p224_public_compressed.der")
+ value = extract_bitstring_from_spki(key_file_compressed_ref)
+ self.assertEqual(value, encoded)
+
+ def test_export_rfc5915_private_der(self):
+ key_file = load_file("ecc_p224_private.der")
+
+ encoded = self.ref_private._export_rfc5915_private_der()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER", use_pkcs8=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_clear(self):
+ key_file = load_file("ecc_p224_private_p8_clear.der")
+
+ encoded = self.ref_private._export_pkcs8()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_encrypted(self):
+ encoded = self.ref_private._export_pkcs8(passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC._import_pkcs8, encoded, None)
+
+ decoded = ECC._import_pkcs8(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA512-224AndAES128-CBC",
+ prot_params={'iteration_count':123})
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_public_pem_uncompressed(self):
+ key_file = load_file("ecc_p224_public.pem", "rt").strip()
+
+ encoded = self.ref_private._export_public_pem(False)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_public.export_key(format="PEM")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="PEM", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_pem_compressed(self):
+ key_file = load_file("ecc_p224_public.pem", "rt").strip()
+ pub_key = ECC.import_key(key_file)
+
+ key_file_compressed = pub_key.export_key(format="PEM", compress=True)
+ key_file_compressed_ref = load_file("ecc_p224_public_compressed.pem", "rt").strip()
+
+ self.assertEqual(key_file_compressed, key_file_compressed_ref)
+
+ def test_export_private_pem_clear(self):
+ key_file = load_file("ecc_p224_private.pem", "rt").strip()
+
+ encoded = self.ref_private._export_private_pem(None)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM", use_pkcs8=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pem_encrypted(self):
+ encoded = self.ref_private._export_private_pem(passphrase=b"secret")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "EC PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ use_pkcs8=False)
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_private_pkcs8_and_pem_1(self):
+ # PKCS8 inside PEM with both unencrypted
+ key_file = load_file("ecc_p224_private_p8_clear.pem", "rt").strip()
+
+ encoded = self.ref_private._export_private_clear_pkcs8_in_clear_pem()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM")
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_and_pem_2(self):
+ # PKCS8 inside PEM with PKCS8 encryption
+ encoded = self.ref_private._export_private_encrypted_pkcs8_in_clear_pem("secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "ENCRYPTED PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_prng(self):
+ # Test that password-protected containers use the provided PRNG
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ # ---
+
+ encoded1 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_byte_or_string_passphrase(self):
+ encoded1 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase=b"secret",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_error_params1(self):
+ # Unknown format
+ self.assertRaises(ValueError, self.ref_private.export_key, format="XXX")
+
+ # Missing 'protection' parameter when PKCS#8 is used
+ self.ref_private.export_key(format="PEM", passphrase="secret",
+ use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="secret")
+
+ # DER format but no PKCS#8
+ self.assertRaises(ValueError, self.ref_private.export_key, format="DER",
+ passphrase="secret",
+ use_pkcs8=False,
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # Incorrect parameters for public keys
+ self.assertRaises(ValueError, self.ref_public.export_key, format="DER",
+ use_pkcs8=False)
+
+ # Empty password
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="", use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ def test_compressed_curve(self):
+
+ # Compressed P-224 curve (Y-point is even)
+ pem1 = """-----BEGIN EC PRIVATE KEY-----
+ MGgCAQEEHPYicBNI9nd6wDKAX2l+f3A0Q+KWUQeMqSt5GoOgBwYFK4EEACGhPAM6
+ AATCL6rUIDT14zXKoS5GQUMDP/tpc+1iI/FyEZikt2roKDkhU5q08srmqaysbfJN
+ eUr7Xf1lnCVGag==
+ -----END EC PRIVATE KEY-----"""
+
+ # Compressed P-224 curve (Y-point is odd)
+ pem2 = """-----BEGIN EC PRIVATE KEY-----
+ MGgCAQEEHEFjbaVPLJ3ngZyCibCvT0RLUqSlHjC5Z3e0FtugBwYFK4EEACGhPAM6
+ AAT5IvL2V6m48y1JLMGr6ZbnOqNKP9hMf9mxyVkk6/SaRoBoJVkXrNIpYL0P7DS7
+ QF8E/OGeZRwvow==
+ -----END EC PRIVATE KEY-----"""
+
+ key1 = ECC.import_key(pem1)
+ low16 = int(key1.pointQ.y % 65536)
+ self.assertEqual(low16, 0x466A)
+
+ key2 = ECC.import_key(pem2)
+ low16 = int(key2.pointQ.y % 65536)
+ self.assertEqual(low16, 0x2FA3)
+
+
+class TestExport_P256(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestExport_P256, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_p256()
+
+ def test_export_public_der_uncompressed(self):
+ key_file = load_file("ecc_p256_public.der")
+
+ encoded = self.ref_public._export_subjectPublicKeyInfo(False)
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_der_compressed(self):
+ key_file = load_file("ecc_p256_public.der")
+ pub_key = ECC.import_key(key_file)
+ key_file_compressed = pub_key.export_key(format="DER", compress=True)
+
+ key_file_compressed_ref = load_file("ecc_p256_public_compressed.der")
+ self.assertEqual(key_file_compressed, key_file_compressed_ref)
+
+ def test_export_public_sec1_uncompressed(self):
+ key_file = load_file("ecc_p256_public.der")
+ value = extract_bitstring_from_spki(key_file)
+
+ encoded = self.ref_public.export_key(format="SEC1")
+ self.assertEqual(value, encoded)
+
+ def test_export_public_sec1_compressed(self):
+ key_file = load_file("ecc_p256_public.der")
+ encoded = self.ref_public.export_key(format="SEC1", compress=True)
+
+ key_file_compressed_ref = load_file("ecc_p256_public_compressed.der")
+ value = extract_bitstring_from_spki(key_file_compressed_ref)
+ self.assertEqual(value, encoded)
+
+ def test_export_rfc5915_private_der(self):
+ key_file = load_file("ecc_p256_private.der")
+
+ encoded = self.ref_private._export_rfc5915_private_der()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER", use_pkcs8=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_clear(self):
+ key_file = load_file("ecc_p256_private_p8_clear.der")
+
+ encoded = self.ref_private._export_pkcs8()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_encrypted(self):
+ encoded = self.ref_private._export_pkcs8(passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC._import_pkcs8, encoded, None)
+
+ decoded = ECC._import_pkcs8(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA512-256AndAES128-CBC",
+ prot_params={'iteration_count':123})
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_public_pem_uncompressed(self):
+ key_file = load_file("ecc_p256_public.pem", "rt").strip()
+
+ encoded = self.ref_private._export_public_pem(False)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_public.export_key(format="PEM")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="PEM", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_pem_compressed(self):
+ key_file = load_file("ecc_p256_public.pem", "rt").strip()
+ pub_key = ECC.import_key(key_file)
+
+ key_file_compressed = pub_key.export_key(format="PEM", compress=True)
+ key_file_compressed_ref = load_file("ecc_p256_public_compressed.pem", "rt").strip()
+
+ self.assertEqual(key_file_compressed, key_file_compressed_ref)
+
+ def test_export_private_pem_clear(self):
+ key_file = load_file("ecc_p256_private.pem", "rt").strip()
+
+ encoded = self.ref_private._export_private_pem(None)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM", use_pkcs8=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pem_encrypted(self):
+ encoded = self.ref_private._export_private_pem(passphrase=b"secret")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "EC PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ use_pkcs8=False)
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_private_pkcs8_and_pem_1(self):
+ # PKCS8 inside PEM with both unencrypted
+ key_file = load_file("ecc_p256_private_p8_clear.pem", "rt").strip()
+
+ encoded = self.ref_private._export_private_clear_pkcs8_in_clear_pem()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM")
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_and_pem_2(self):
+ # PKCS8 inside PEM with PKCS8 encryption
+ encoded = self.ref_private._export_private_encrypted_pkcs8_in_clear_pem("secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "ENCRYPTED PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_openssh_uncompressed(self):
+ key_file = load_file("ecc_p256_public_openssh.txt", "rt")
+
+ encoded = self.ref_public._export_openssh(False)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_public.export_key(format="OpenSSH")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="OpenSSH", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_openssh_compressed(self):
+ key_file = load_file("ecc_p256_public_openssh.txt", "rt")
+ pub_key = ECC.import_key(key_file)
+
+ key_file_compressed = pub_key.export_key(format="OpenSSH", compress=True)
+ assert len(key_file) > len(key_file_compressed)
+ self.assertEqual(pub_key, ECC.import_key(key_file_compressed))
+
+ def test_prng(self):
+ # Test that password-protected containers use the provided PRNG
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ # ---
+
+ encoded1 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_byte_or_string_passphrase(self):
+ encoded1 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase=b"secret",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_error_params1(self):
+ # Unknown format
+ self.assertRaises(ValueError, self.ref_private.export_key, format="XXX")
+
+ # Missing 'protection' parameter when PKCS#8 is used
+ self.ref_private.export_key(format="PEM", passphrase="secret",
+ use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="secret")
+
+ # DER format but no PKCS#8
+ self.assertRaises(ValueError, self.ref_private.export_key, format="DER",
+ passphrase="secret",
+ use_pkcs8=False,
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # Incorrect parameters for public keys
+ self.assertRaises(ValueError, self.ref_public.export_key, format="DER",
+ use_pkcs8=False)
+
+ # Empty password
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="", use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # No private keys with OpenSSH
+ self.assertRaises(ValueError, self.ref_private.export_key, format="OpenSSH",
+ passphrase="secret")
+
+
+ def test_compressed_curve(self):
+
+ # Compressed P-256 curve (Y-point is even)
+ pem1 = """-----BEGIN EC PRIVATE KEY-----
+ MFcCAQEEIHTuc09jC51xXomV6MVCDN+DpAAvSmaJWZPTEHM6D5H1oAoGCCqGSM49
+ AwEHoSQDIgACWFuGbHe8yJ43rir7PMTE9w8vHz0BSpXHq90Xi7/s+a0=
+ -----END EC PRIVATE KEY-----"""
+
+ # Compressed P-256 curve (Y-point is odd)
+ pem2 = """-----BEGIN EC PRIVATE KEY-----
+ MFcCAQEEIFggiPN9SQP+FAPTCPp08fRUz7rHp2qNBRcBJ1DXhb3ZoAoGCCqGSM49
+ AwEHoSQDIgADLpph1trTIlVfa8NJvlMUPyWvL+wP+pW3BJITUL/wj9A=
+ -----END EC PRIVATE KEY-----"""
+
+ key1 = ECC.import_key(pem1)
+ low16 = int(key1.pointQ.y % 65536)
+ self.assertEqual(low16, 0xA6FC)
+
+ key2 = ECC.import_key(pem2)
+ low16 = int(key2.pointQ.y % 65536)
+ self.assertEqual(low16, 0x6E57)
+
+
+class TestExport_P384(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestExport_P384, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_p384()
+
+ def test_export_public_der_uncompressed(self):
+ key_file = load_file("ecc_p384_public.der")
+
+ encoded = self.ref_public._export_subjectPublicKeyInfo(False)
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_der_compressed(self):
+ key_file = load_file("ecc_p384_public.der")
+ pub_key = ECC.import_key(key_file)
+ key_file_compressed = pub_key.export_key(format="DER", compress=True)
+
+ key_file_compressed_ref = load_file("ecc_p384_public_compressed.der")
+ self.assertEqual(key_file_compressed, key_file_compressed_ref)
+
+ def test_export_public_sec1_uncompressed(self):
+ key_file = load_file("ecc_p384_public.der")
+ value = extract_bitstring_from_spki(key_file)
+
+ encoded = self.ref_public.export_key(format="SEC1")
+ self.assertEqual(value, encoded)
+
+ def test_export_public_sec1_compressed(self):
+ key_file = load_file("ecc_p384_public.der")
+ encoded = self.ref_public.export_key(format="SEC1", compress=True)
+
+ key_file_compressed_ref = load_file("ecc_p384_public_compressed.der")
+ value = extract_bitstring_from_spki(key_file_compressed_ref)
+ self.assertEqual(value, encoded)
+
+ def test_export_rfc5915_private_der(self):
+ key_file = load_file("ecc_p384_private.der")
+
+ encoded = self.ref_private._export_rfc5915_private_der()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER", use_pkcs8=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_clear(self):
+ key_file = load_file("ecc_p384_private_p8_clear.der")
+
+ encoded = self.ref_private._export_pkcs8()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_encrypted(self):
+ encoded = self.ref_private._export_pkcs8(passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC._import_pkcs8, encoded, None)
+
+ decoded = ECC._import_pkcs8(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA384AndAES128-CBC",
+ prot_params={'iteration_count':123})
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_public_pem_uncompressed(self):
+ key_file = load_file("ecc_p384_public.pem", "rt").strip()
+
+ encoded = self.ref_private._export_public_pem(False)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_public.export_key(format="PEM")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="PEM", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_pem_compressed(self):
+ key_file = load_file("ecc_p384_public.pem", "rt").strip()
+ pub_key = ECC.import_key(key_file)
+
+ key_file_compressed = pub_key.export_key(format="PEM", compress=True)
+ key_file_compressed_ref = load_file("ecc_p384_public_compressed.pem", "rt").strip()
+
+ self.assertEqual(key_file_compressed, key_file_compressed_ref)
+
+ def test_export_private_pem_clear(self):
+ key_file = load_file("ecc_p384_private.pem", "rt").strip()
+
+ encoded = self.ref_private._export_private_pem(None)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM", use_pkcs8=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pem_encrypted(self):
+ encoded = self.ref_private._export_private_pem(passphrase=b"secret")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "EC PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ use_pkcs8=False)
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_private_pkcs8_and_pem_1(self):
+ # PKCS8 inside PEM with both unencrypted
+ key_file = load_file("ecc_p384_private_p8_clear.pem", "rt").strip()
+
+ encoded = self.ref_private._export_private_clear_pkcs8_in_clear_pem()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM")
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_and_pem_2(self):
+ # PKCS8 inside PEM with PKCS8 encryption
+ encoded = self.ref_private._export_private_encrypted_pkcs8_in_clear_pem("secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "ENCRYPTED PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_openssh_uncompressed(self):
+ key_file = load_file("ecc_p384_public_openssh.txt", "rt")
+
+ encoded = self.ref_public._export_openssh(False)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_public.export_key(format="OpenSSH")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="OpenSSH", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_openssh_compressed(self):
+ key_file = load_file("ecc_p384_public_openssh.txt", "rt")
+ pub_key = ECC.import_key(key_file)
+
+ key_file_compressed = pub_key.export_key(format="OpenSSH", compress=True)
+ assert len(key_file) > len(key_file_compressed)
+ self.assertEqual(pub_key, ECC.import_key(key_file_compressed))
+
+ def test_prng(self):
+ # Test that password-protected containers use the provided PRNG
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ # ---
+
+ encoded1 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_byte_or_string_passphrase(self):
+ encoded1 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase=b"secret",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_error_params1(self):
+ # Unknown format
+ self.assertRaises(ValueError, self.ref_private.export_key, format="XXX")
+
+ # Missing 'protection' parameter when PKCS#8 is used
+ self.ref_private.export_key(format="PEM", passphrase="secret",
+ use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="secret")
+
+ # DER format but no PKCS#8
+ self.assertRaises(ValueError, self.ref_private.export_key, format="DER",
+ passphrase="secret",
+ use_pkcs8=False,
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # Incorrect parameters for public keys
+ self.assertRaises(ValueError, self.ref_public.export_key, format="DER",
+ use_pkcs8=False)
+
+ # Empty password
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="", use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # No private keys with OpenSSH
+ self.assertRaises(ValueError, self.ref_private.export_key, format="OpenSSH",
+ passphrase="secret")
+
+ def test_compressed_curve(self):
+
+ # Compressed P-384 curve (Y-point is even)
+ # openssl ecparam -name secp384p1 -genkey -noout -conv_form compressed -out /tmp/a.pem
+ # openssl ec -in /tmp/a.pem -text -noout
+ pem1 = """-----BEGIN EC PRIVATE KEY-----
+MIGkAgEBBDAM0lEIhvXuekK2SWtdbgOcZtBaxa9TxfpO/GcDFZLCJ3JVXaTgwken
+QT+C+XLtD6WgBwYFK4EEACKhZANiAATs0kZMhFDu8DoBC21jrSDPyAUn4aXZ/DM4
+ylhDfWmb4LEbeszXceIzfhIUaaGs5y1xXaqf5KXTiAAYx2pKUzAAM9lcGUHCGKJG
+k4AgUmVJON29XoUilcFrzjDmuye3B6Q=
+-----END EC PRIVATE KEY-----"""
+
+ # Compressed P-384 curve (Y-point is odd)
+ pem2 = """-----BEGIN EC PRIVATE KEY-----
+MIGkAgEBBDDHPFTslYLltE16fHdSDTtE/2HTmd3M8mqy5MttAm4wZ833KXiGS9oe
+kFdx9sNV0KygBwYFK4EEACKhZANiAASLIE5RqVMtNhtBH/u/p/ifqOAlKnK/+RrQ
+YC46ZRsnKNayw3wATdPjgja7L/DSII3nZK0G6KOOVwJBznT/e+zudUJYhZKaBLRx
+/bgXyxUtYClOXxb1Y/5N7txLstYRyP0=
+-----END EC PRIVATE KEY-----"""
+
+ key1 = ECC.import_key(pem1)
+ low16 = int(key1.pointQ.y % 65536)
+ self.assertEqual(low16, 0x07a4)
+
+ key2 = ECC.import_key(pem2)
+ low16 = int(key2.pointQ.y % 65536)
+ self.assertEqual(low16, 0xc8fd)
+
+
+class TestExport_P521(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestExport_P521, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_p521()
+
+ def test_export_public_der_uncompressed(self):
+ key_file = load_file("ecc_p521_public.der")
+
+ encoded = self.ref_public._export_subjectPublicKeyInfo(False)
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_der_compressed(self):
+ key_file = load_file("ecc_p521_public.der")
+ pub_key = ECC.import_key(key_file)
+ key_file_compressed = pub_key.export_key(format="DER", compress=True)
+
+ key_file_compressed_ref = load_file("ecc_p521_public_compressed.der")
+ self.assertEqual(key_file_compressed, key_file_compressed_ref)
+
+ def test_export_public_sec1_uncompressed(self):
+ key_file = load_file("ecc_p521_public.der")
+ value = extract_bitstring_from_spki(key_file)
+
+ encoded = self.ref_public.export_key(format="SEC1")
+ self.assertEqual(value, encoded)
+
+ encoded = self.ref_public.export_key(format="raw")
+ self.assertEqual(value, encoded)
+
+ def test_export_public_sec1_compressed(self):
+ key_file = load_file("ecc_p521_public.der")
+ encoded = self.ref_public.export_key(format="SEC1", compress=True)
+
+ key_file_compressed_ref = load_file("ecc_p521_public_compressed.der")
+ value = extract_bitstring_from_spki(key_file_compressed_ref)
+ self.assertEqual(value, encoded)
+
+ encoded = self.ref_public.export_key(format="raw", compress=True)
+ self.assertEqual(value, encoded)
+
+ def test_export_rfc5915_private_der(self):
+ key_file = load_file("ecc_p521_private.der")
+
+ encoded = self.ref_private._export_rfc5915_private_der()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER", use_pkcs8=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_clear(self):
+ key_file = load_file("ecc_p521_private_p8_clear.der")
+
+ encoded = self.ref_private._export_pkcs8()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_encrypted(self):
+ encoded = self.ref_private._export_pkcs8(passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC._import_pkcs8, encoded, None)
+
+ decoded = ECC._import_pkcs8(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ prot_params={'iteration_count':123})
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_public_pem_uncompressed(self):
+ key_file = load_file("ecc_p521_public.pem", "rt").strip()
+
+ encoded = self.ref_private._export_public_pem(False)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_public.export_key(format="PEM")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="PEM", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_pem_compressed(self):
+ key_file = load_file("ecc_p521_public.pem", "rt").strip()
+ pub_key = ECC.import_key(key_file)
+
+ key_file_compressed = pub_key.export_key(format="PEM", compress=True)
+ key_file_compressed_ref = load_file("ecc_p521_public_compressed.pem", "rt").strip()
+
+ self.assertEqual(key_file_compressed, key_file_compressed_ref)
+
+ def test_export_private_pem_clear(self):
+ key_file = load_file("ecc_p521_private.pem", "rt").strip()
+
+ encoded = self.ref_private._export_private_pem(None)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM", use_pkcs8=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pem_encrypted(self):
+ encoded = self.ref_private._export_private_pem(passphrase=b"secret")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "EC PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ use_pkcs8=False)
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_private_pkcs8_and_pem_1(self):
+ # PKCS8 inside PEM with both unencrypted
+ key_file = load_file("ecc_p521_private_p8_clear.pem", "rt").strip()
+
+ encoded = self.ref_private._export_private_clear_pkcs8_in_clear_pem()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM")
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pkcs8_and_pem_2(self):
+ # PKCS8 inside PEM with PKCS8 encryption
+ encoded = self.ref_private._export_private_encrypted_pkcs8_in_clear_pem("secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "ENCRYPTED PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_openssh_uncompressed(self):
+ key_file = load_file("ecc_p521_public_openssh.txt", "rt")
+
+ encoded = self.ref_public._export_openssh(False)
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_public.export_key(format="OpenSSH")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="OpenSSH", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_openssh_compressed(self):
+ key_file = load_file("ecc_p521_public_openssh.txt", "rt")
+ pub_key = ECC.import_key(key_file)
+
+ key_file_compressed = pub_key.export_key(format="OpenSSH", compress=True)
+ assert len(key_file) > len(key_file_compressed)
+ self.assertEqual(pub_key, ECC.import_key(key_file_compressed))
+
+ def test_prng(self):
+ # Test that password-protected containers use the provided PRNG
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ # ---
+
+ encoded1 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_byte_or_string_passphrase(self):
+ encoded1 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase="secret",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ use_pkcs8=False,
+ passphrase=b"secret",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_error_params1(self):
+ # Unknown format
+ self.assertRaises(ValueError, self.ref_private.export_key, format="XXX")
+
+ # Missing 'protection' parameter when PKCS#8 is used
+ self.ref_private.export_key(format="PEM", passphrase="secret",
+ use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="secret")
+
+ # DER format but no PKCS#8
+ self.assertRaises(ValueError, self.ref_private.export_key, format="DER",
+ passphrase="secret",
+ use_pkcs8=False,
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # Incorrect parameters for public keys
+ self.assertRaises(ValueError, self.ref_public.export_key, format="DER",
+ use_pkcs8=False)
+
+ # Empty password
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="", use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # No private keys with OpenSSH
+ self.assertRaises(ValueError, self.ref_private.export_key, format="OpenSSH",
+ passphrase="secret")
+
+ def test_compressed_curve(self):
+
+ # Compressed P-521 curve (Y-point is even)
+ # openssl ecparam -name secp521r1 -genkey -noout -conv_form compressed -out /tmp/a.pem
+ # openssl ec -in /tmp/a.pem -text -noout
+ pem1 = """-----BEGIN EC PRIVATE KEY-----
+MIHcAgEBBEIAnm1CEjVjvNfXEN730p+D6su5l+mOztdc5XmTEoti+s2R4GQ4mAv3
+0zYLvyklvOHw0+yy8d0cyGEJGb8T3ZVKmg2gBwYFK4EEACOhgYkDgYYABAHzjTI1
+ckxQ3Togi0LAxiG0PucdBBBs5oIy3df95xv6SInp70z+4qQ2EltEmdNMssH8eOrl
+M5CYdZ6nbcHMVaJUvQEzTrYxvFjOgJiOd+E9eBWbLkbMNqsh1UKVO6HbMbW0ohCI
+uGxO8tM6r3w89/qzpG2SvFM/fvv3mIR30wSZDD84qA==
+-----END EC PRIVATE KEY-----"""
+
+ # Compressed P-521 curve (Y-point is odd)
+ pem2 = """-----BEGIN EC PRIVATE KEY-----
+MIHcAgEBBEIB84OfhJluLBRLn3+cC/RQ37C2SfQVP/t0gQK2tCsTf5avRcWYRrOJ
+PmX9lNnkC0Hobd75QFRmdxrB0Wd1/M4jZOWgBwYFK4EEACOhgYkDgYYABAAMZcdJ
+1YLCGHt3bHCEzdidVy6+brlJIbv1aQ9fPQLF7WKNv4c8w3H8d5a2+SDZilBOsk5c
+6cNJDMz2ExWQvxl4CwDJtJGt1+LHVKFGy73NANqVxMbRu+2F8lOxkNp/ziFTbVyV
+vv6oYkMIIi7r5oQWAiQDrR2mlrrFDL9V7GH/r8SWQw==
+-----END EC PRIVATE KEY-----"""
+
+ key1 = ECC.import_key(pem1)
+ low16 = int(key1.pointQ.y % 65536)
+ self.assertEqual(low16, 0x38a8)
+
+ key2 = ECC.import_key(pem2)
+ low16 = int(key2.pointQ.y % 65536)
+ self.assertEqual(low16, 0x9643)
+
+
+class TestImport_Ed25519(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestImport_Ed25519, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_ed25519()
+
+ def test_import_public_der(self):
+ key_file = load_file("ecc_ed25519_public.der")
+
+ key = ECC._import_subjectPublicKeyInfo(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_pkcs8_der(self):
+ key_file = load_file("ecc_ed25519_private.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_1(self):
+ key_file = load_file("ecc_ed25519_private_p8.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_2(self):
+ key_file = load_file("ecc_ed25519_private_p8.pem")
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_3(self):
+ key_file = load_file("ecc_ed25519_private_p8_2.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_der(self):
+ key_file = load_file("ecc_ed25519_x509.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_public_pem(self):
+ key_file = load_file("ecc_ed25519_public.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_private_pem(self):
+ key_file = load_file("ecc_ed25519_private.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pem_encrypted(self):
+ for algo in "des3", "aes128", "aes192", "aes256":
+ key_file = load_file("ecc_ed25519_private_enc_%s.pem" % algo)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(tostr(key_file), b"secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_pem(self):
+ key_file = load_file("ecc_ed25519_x509.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_openssh_public(self):
+ key_file = load_file("ecc_ed25519_public_openssh.txt")
+ key = ECC._import_openssh_public(key_file)
+ self.assertFalse(key.has_private())
+ key = ECC.import_key(key_file)
+ self.assertFalse(key.has_private())
+
+ def test_import_openssh_private_clear(self):
+ key_file = load_file("ecc_ed25519_private_openssh.pem")
+ key = ECC.import_key(key_file)
+
+ def test_import_openssh_private_password(self):
+ key_file = load_file("ecc_ed25519_private_openssh_pwd.pem")
+ key = ECC.import_key(key_file, b"password")
+
+
+class TestExport_Ed25519(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestExport_Ed25519, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_ed25519()
+
+ def test_export_public_der(self):
+ key_file = load_file("ecc_ed25519_public.der")
+
+ encoded = self.ref_public._export_subjectPublicKeyInfo(True)
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_sec1(self):
+ self.assertRaises(ValueError, self.ref_public.export_key, format="SEC1")
+
+ def test_export_private_pkcs8_clear(self):
+ key_file = load_file("ecc_ed25519_private.der")
+
+ encoded = self.ref_private._export_pkcs8()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ self.assertRaises(ValueError, self.ref_private.export_key,
+ format="DER", use_pkcs8=False)
+
+ def test_export_private_pkcs8_encrypted(self):
+ encoded = self.ref_private._export_pkcs8(passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC._import_pkcs8, encoded, None)
+
+ decoded = ECC._import_pkcs8(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA256AndAES128-CBC",
+ prot_params={'iteration_count':123})
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_public_pem(self):
+ key_file_ref = load_file("ecc_ed25519_public.pem", "rt").strip()
+ key_file = self.ref_public.export_key(format="PEM").strip()
+ self.assertEqual(key_file_ref, key_file)
+
+ def test_export_private_pem_clear(self):
+ key_file = load_file("ecc_ed25519_private.pem", "rt").strip()
+ encoded = self.ref_private.export_key(format="PEM").strip()
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pem_encrypted(self):
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase=b"secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "ENCRYPTED PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_openssh(self):
+ key_file = load_file("ecc_ed25519_public_openssh.txt", "rt")
+ public_key = ECC.import_key(key_file)
+ key_file = " ".join(key_file.split(' ')[:2]) # remove comment
+
+ encoded = public_key._export_openssh(False)
+ self.assertEqual(key_file, encoded.strip())
+
+ encoded = public_key.export_key(format="OpenSSH")
+ self.assertEqual(key_file, encoded.strip())
+
+ def test_export_raw(self):
+ encoded = self.ref_public.export_key(format='raw')
+ self.assertEqual(encoded, unhexlify(b'bc85b8cf585d20a4de47e84d1cb6183f63d9ba96223fcbc886e363ffdea20cff'))
+
+ def test_prng(self):
+ # Test that password-protected containers use the provided PRNG
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_byte_or_string_passphrase(self):
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase=b"secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_error_params1(self):
+ # Unknown format
+ self.assertRaises(ValueError, self.ref_private.export_key, format="XXX")
+
+ # Missing 'protection' parameter when PKCS#8 is used
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="secret")
+
+ # Empty password
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="", use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # No private keys with OpenSSH
+ self.assertRaises(ValueError, self.ref_private.export_key, format="OpenSSH",
+ passphrase="secret")
+
+
+class TestImport_Ed448(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestImport_Ed448, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_ed448()
+
+ def test_import_public_der(self):
+ key_file = load_file("ecc_ed448_public.der")
+
+ key = ECC._import_subjectPublicKeyInfo(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_pkcs8_der(self):
+ key_file = load_file("ecc_ed448_private.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_1(self):
+ key_file = load_file("ecc_ed448_private_p8.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_2(self):
+ key_file = load_file("ecc_ed448_private_p8.pem")
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pkcs8_encrypted_3(self):
+ key_file = load_file("ecc_ed448_private_p8_2.der")
+
+ key = ECC._import_der(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_der(self):
+ key_file = load_file("ecc_ed448_x509.der")
+
+ key = ECC._import_der(key_file, None)
+ self.assertEqual(self.ref_public, key)
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_public_pem(self):
+ key_file = load_file("ecc_ed448_public.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+ def test_import_private_pem(self):
+ key_file = load_file("ecc_ed448_private.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_private_pem_encrypted(self):
+ for algo in "des3", "aes128", "aes192", "aes256":
+ key_file = load_file("ecc_ed448_private_enc_%s.pem" % algo)
+
+ key = ECC.import_key(key_file, "secret")
+ self.assertEqual(self.ref_private, key)
+
+ key = ECC.import_key(tostr(key_file), b"secret")
+ self.assertEqual(self.ref_private, key)
+
+ def test_import_x509_pem(self):
+ key_file = load_file("ecc_ed448_x509.pem")
+
+ key = ECC.import_key(key_file)
+ self.assertEqual(self.ref_public, key)
+
+
+class TestExport_Ed448(unittest.TestCase):
+
+ def __init__(self, *args, **kwargs):
+ super(TestExport_Ed448, self).__init__(*args, **kwargs)
+ self.ref_private, self.ref_public = create_ref_keys_ed448()
+
+ def test_export_public_der(self):
+ key_file = load_file("ecc_ed448_public.der")
+
+ encoded = self.ref_public._export_subjectPublicKeyInfo(True)
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ encoded = self.ref_public.export_key(format="DER", compress=False)
+ self.assertEqual(key_file, encoded)
+
+ def test_export_public_sec1(self):
+ self.assertRaises(ValueError, self.ref_public.export_key, format="SEC1")
+
+ def test_export_private_pkcs8_clear(self):
+ key_file = load_file("ecc_ed448_private.der")
+
+ encoded = self.ref_private._export_pkcs8()
+ self.assertEqual(key_file, encoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER")
+ self.assertEqual(key_file, encoded)
+
+ self.assertRaises(ValueError, self.ref_private.export_key,
+ format="DER", use_pkcs8=False)
+
+ def test_export_private_pkcs8_encrypted(self):
+ encoded = self.ref_private._export_pkcs8(passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC._import_pkcs8, encoded, None)
+
+ decoded = ECC._import_pkcs8(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ # ---
+
+ encoded = self.ref_private.export_key(format="DER",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA384AndAES128-CBC",
+ prot_params={'iteration_count':123})
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+
+ def test_export_public_pem(self):
+ key_file_ref = load_file("ecc_ed448_public.pem", "rt").strip()
+ key_file = self.ref_public.export_key(format="PEM").strip()
+ self.assertEqual(key_file_ref, key_file)
+
+ def test_export_private_pem_clear(self):
+ key_file = load_file("ecc_ed448_private.pem", "rt").strip()
+ encoded = self.ref_private.export_key(format="PEM").strip()
+ self.assertEqual(key_file, encoded)
+
+ def test_export_private_pem_encrypted(self):
+ encoded = self.ref_private.export_key(format="PEM",
+ passphrase=b"secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # This should prove that the output is password-protected
+ self.assertRaises(ValueError, ECC.import_key, encoded)
+
+ assert "ENCRYPTED PRIVATE KEY" in encoded
+
+ decoded = ECC.import_key(encoded, "secret")
+ self.assertEqual(self.ref_private, decoded)
+
+ def test_export_openssh(self):
+ # Not supported
+ self.assertRaises(ValueError, self.ref_public.export_key, format="OpenSSH")
+
+ def test_export_raw(self):
+ encoded = self.ref_public.export_key(format='raw')
+ self.assertEqual(encoded, unhexlify(b'899014ddc0a0e1260cfc1085afdf952019e9fd63372e3e366e26dad32b176624884330a14617237e3081febd9d1a15069e7499433d2f55dd80'))
+
+ def test_prng(self):
+ # Test that password-protected containers use the provided PRNG
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_byte_or_string_passphrase(self):
+ encoded1 = self.ref_private.export_key(format="PEM",
+ passphrase="secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ encoded2 = self.ref_private.export_key(format="PEM",
+ passphrase=b"secret",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC",
+ randfunc=get_fixed_prng())
+ self.assertEqual(encoded1, encoded2)
+
+ def test_error_params1(self):
+ # Unknown format
+ self.assertRaises(ValueError, self.ref_private.export_key, format="XXX")
+
+ # Missing 'protection' parameter when PKCS#8 is used
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="secret")
+
+ # Empty password
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="", use_pkcs8=False)
+ self.assertRaises(ValueError, self.ref_private.export_key, format="PEM",
+ passphrase="",
+ protection="PBKDF2WithHMAC-SHA1AndAES128-CBC")
+
+ # No private keys with OpenSSH
+ self.assertRaises(ValueError, self.ref_private.export_key, format="OpenSSH",
+ passphrase="secret")
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(TestImport)
+ try:
+ tests += list_test_cases(TestImport_P192)
+ tests += list_test_cases(TestImport_P224)
+ tests += list_test_cases(TestImport_P256)
+ tests += list_test_cases(TestImport_P384)
+ tests += list_test_cases(TestImport_P521)
+ tests += list_test_cases(TestImport_Ed25519)
+ tests += list_test_cases(TestImport_Ed448)
+
+ tests += list_test_cases(TestExport_P192)
+ tests += list_test_cases(TestExport_P224)
+ tests += list_test_cases(TestExport_P256)
+ tests += list_test_cases(TestExport_P384)
+ tests += list_test_cases(TestExport_P521)
+ tests += list_test_cases(TestExport_Ed25519)
+ tests += list_test_cases(TestExport_Ed448)
+
+ except MissingTestVectorException:
+ pass
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_RSA.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_RSA.py
new file mode 100644
index 00000000..f6f7afd4
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/PublicKey/test_import_RSA.py
@@ -0,0 +1,636 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/PublicKey/test_importKey.py: Self-test for importing RSA keys
+#
+# ===================================================================
+# 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 os
+import re
+import errno
+import warnings
+import unittest
+from unittest import SkipTest
+
+from Crypto.PublicKey import RSA
+from Crypto.SelfTest.st_common import a2b_hex, list_test_cases
+from Crypto.IO import PEM
+from Crypto.Util.py3compat import b, tostr, FileNotFoundError
+from Crypto.Util.number import inverse
+from Crypto.Util import asn1
+
+try:
+ import pycryptodome_test_vectors # type: ignore
+ test_vectors_available = True
+except ImportError:
+ test_vectors_available = False
+
+
+def load_file(file_name, mode="rb"):
+ results = None
+
+ try:
+ if not test_vectors_available:
+ raise FileNotFoundError(errno.ENOENT,
+ os.strerror(errno.ENOENT),
+ file_name)
+
+ dir_comps = ("PublicKey", "RSA")
+ init_dir = os.path.dirname(pycryptodome_test_vectors.__file__)
+ full_file_name = os.path.join(os.path.join(init_dir, *dir_comps), file_name)
+ with open(full_file_name, mode) as file_in:
+ results = file_in.read()
+
+ except FileNotFoundError:
+ warnings.warn("Skipping tests for RSA based on %s" % file_name,
+ UserWarning,
+ stacklevel=2)
+
+ if results is None:
+ raise SkipTest("Missing %s" % file_name)
+
+ return results
+
+
+def der2pem(der, text='PUBLIC'):
+ import binascii
+ chunks = [binascii.b2a_base64(der[i:i+48]) for i in range(0, len(der), 48)]
+ pem = b('-----BEGIN %s KEY-----\n' % text)
+ pem += b('').join(chunks)
+ pem += b('-----END %s KEY-----' % text)
+ return pem
+
+
+class ImportKeyTests(unittest.TestCase):
+ # 512-bit RSA key generated with openssl
+ rsaKeyPEM = u'''-----BEGIN RSA PRIVATE KEY-----
+MIIBOwIBAAJBAL8eJ5AKoIsjURpcEoGubZMxLD7+kT+TLr7UkvEtFrRhDDKMtuII
+q19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQJACUSDEp8RTe32ftq8IwG8
+Wojl5mAd1wFiIOrZ/Uv8b963WJOJiuQcVN29vxU5+My9GPZ7RA3hrDBEAoHUDPrI
+OQIhAPIPLz4dphiD9imAkivY31Rc5AfHJiQRA7XixTcjEkojAiEAyh/pJHks/Mlr
++rdPNEpotBjfV4M4BkgGAA/ipcmaAjcCIQCHvhwwKVBLzzTscT2HeUdEeBMoiXXK
+JACAr3sJQJGxIQIgarRp+m1WSKV1MciwMaTOnbU7wxFs9DP1pva76lYBzgUCIQC9
+n0CnZCJ6IZYqSt0H5N7+Q+2Ro64nuwV/OSQfM6sBwQ==
+-----END RSA PRIVATE KEY-----'''
+
+ # As above, but this is actually an unencrypted PKCS#8 key
+ rsaKeyPEM8 = u'''-----BEGIN PRIVATE KEY-----
+MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAvx4nkAqgiyNRGlwS
+ga5tkzEsPv6RP5MuvtSS8S0WtGEMMoy24girX0WsvilQgzKY8xIsGfeEkt7fQPDj
+wZAzhQIDAQABAkAJRIMSnxFN7fZ+2rwjAbxaiOXmYB3XAWIg6tn9S/xv3rdYk4mK
+5BxU3b2/FTn4zL0Y9ntEDeGsMEQCgdQM+sg5AiEA8g8vPh2mGIP2KYCSK9jfVFzk
+B8cmJBEDteLFNyMSSiMCIQDKH+kkeSz8yWv6t080Smi0GN9XgzgGSAYAD+KlyZoC
+NwIhAIe+HDApUEvPNOxxPYd5R0R4EyiJdcokAICvewlAkbEhAiBqtGn6bVZIpXUx
+yLAxpM6dtTvDEWz0M/Wm9rvqVgHOBQIhAL2fQKdkInohlipK3Qfk3v5D7ZGjrie7
+BX85JB8zqwHB
+-----END PRIVATE KEY-----'''
+
+ # The same RSA private key as in rsaKeyPEM, but now encrypted
+ rsaKeyEncryptedPEM = (
+
+ # PEM encryption
+ # With DES and passphrase 'test'
+ ('test', u'''-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-CBC,AF8F9A40BD2FA2FC
+
+Ckl9ex1kaVEWhYC2QBmfaF+YPiR4NFkRXA7nj3dcnuFEzBnY5XULupqQpQI3qbfA
+u8GYS7+b3toWWiHZivHbAAUBPDIZG9hKDyB9Sq2VMARGsX1yW1zhNvZLIiVJzUHs
+C6NxQ1IJWOXzTew/xM2I26kPwHIvadq+/VaT8gLQdjdH0jOiVNaevjWnLgrn1mLP
+BCNRMdcexozWtAFNNqSzfW58MJL2OdMi21ED184EFytIc1BlB+FZiGZduwKGuaKy
+9bMbdb/1PSvsSzPsqW7KSSrTw6MgJAFJg6lzIYvR5F4poTVBxwBX3+EyEmShiaNY
+IRX3TgQI0IjrVuLmvlZKbGWP18FXj7I7k9tSsNOOzllTTdq3ny5vgM3A+ynfAaxp
+dysKznQ6P+IoqML1WxAID4aGRMWka+uArOJ148Rbj9s=
+-----END RSA PRIVATE KEY-----'''),
+
+ # PKCS8 encryption
+ ('winter', u'''-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIIBpjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIeZIsbW3O+JcCAggA
+MBQGCCqGSIb3DQMHBAgSM2p0D8FilgSCAWBhFyP2tiGKVpGj3mO8qIBzinU60ApR
+3unvP+N6j7LVgnV2lFGaXbJ6a1PbQXe+2D6DUyBLo8EMXrKKVLqOMGkFMHc0UaV6
+R6MmrsRDrbOqdpTuVRW+NVd5J9kQQh4xnfU/QrcPPt7vpJvSf4GzG0n666Ki50OV
+M/feuVlIiyGXY6UWdVDpcOV72cq02eNUs/1JWdh2uEBvA9fCL0c07RnMrdT+CbJQ
+NjJ7f8ULtp7xvR9O3Al/yJ4Wv3i4VxF1f3MCXzhlUD4I0ONlr0kJWgeQ80q/cWhw
+ntvgJwnCn2XR1h6LA8Wp+0ghDTsL2NhJpWd78zClGhyU4r3hqu1XDjoXa7YCXCix
+jCV15+ViDJzlNCwg+W6lRg18sSLkCT7alviIE0U5tHc6UPbbHwT5QqAxAABaP+nZ
+CGqJGyiwBzrKebjgSm/KRd4C91XqcsysyH2kKPfT51MLAoD4xelOURBP
+-----END ENCRYPTED PRIVATE KEY-----'''
+ ),
+ )
+
+ rsaPublicKeyPEM = u'''-----BEGIN PUBLIC KEY-----
+MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAL8eJ5AKoIsjURpcEoGubZMxLD7+kT+T
+Lr7UkvEtFrRhDDKMtuIIq19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQ==
+-----END PUBLIC KEY-----'''
+
+ # Obtained using 'ssh-keygen -i -m PKCS8 -f rsaPublicKeyPEM'
+ rsaPublicKeyOpenSSH = b('''ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQC/HieQCqCLI1EaXBKBrm2TMSw+/pE/ky6+1JLxLRa0YQwyjLbiCKtfRay+KVCDMpjzEiwZ94SS3t9A8OPBkDOF comment\n''')
+
+ # The private key, in PKCS#1 format encoded with DER
+ rsaKeyDER = a2b_hex(
+ '''3082013b020100024100bf1e27900aa08b23511a5c1281ae6d93312c3efe
+ 913f932ebed492f12d16b4610c328cb6e208ab5f45acbe2950833298f312
+ 2c19f78492dedf40f0e3c190338502030100010240094483129f114dedf6
+ 7edabc2301bc5a88e5e6601dd7016220ead9fd4bfc6fdeb75893898ae41c
+ 54ddbdbf1539f8ccbd18f67b440de1ac30440281d40cfac839022100f20f
+ 2f3e1da61883f62980922bd8df545ce407c726241103b5e2c53723124a23
+ 022100ca1fe924792cfcc96bfab74f344a68b418df578338064806000fe2
+ a5c99a023702210087be1c3029504bcf34ec713d877947447813288975ca
+ 240080af7b094091b12102206ab469fa6d5648a57531c8b031a4ce9db53b
+ c3116cf433f5a6f6bbea5601ce05022100bd9f40a764227a21962a4add07
+ e4defe43ed91a3ae27bb057f39241f33ab01c1
+ '''.replace(" ",""))
+
+ # The private key, in unencrypted PKCS#8 format encoded with DER
+ rsaKeyDER8 = a2b_hex(
+ '''30820155020100300d06092a864886f70d01010105000482013f3082013
+ b020100024100bf1e27900aa08b23511a5c1281ae6d93312c3efe913f932
+ ebed492f12d16b4610c328cb6e208ab5f45acbe2950833298f3122c19f78
+ 492dedf40f0e3c190338502030100010240094483129f114dedf67edabc2
+ 301bc5a88e5e6601dd7016220ead9fd4bfc6fdeb75893898ae41c54ddbdb
+ f1539f8ccbd18f67b440de1ac30440281d40cfac839022100f20f2f3e1da
+ 61883f62980922bd8df545ce407c726241103b5e2c53723124a23022100c
+ a1fe924792cfcc96bfab74f344a68b418df578338064806000fe2a5c99a0
+ 23702210087be1c3029504bcf34ec713d877947447813288975ca240080a
+ f7b094091b12102206ab469fa6d5648a57531c8b031a4ce9db53bc3116cf
+ 433f5a6f6bbea5601ce05022100bd9f40a764227a21962a4add07e4defe4
+ 3ed91a3ae27bb057f39241f33ab01c1
+ '''.replace(" ",""))
+
+ rsaPublicKeyDER = a2b_hex(
+ '''305c300d06092a864886f70d0101010500034b003048024100bf1e27900a
+ a08b23511a5c1281ae6d93312c3efe913f932ebed492f12d16b4610c328c
+ b6e208ab5f45acbe2950833298f3122c19f78492dedf40f0e3c190338502
+ 03010001
+ '''.replace(" ",""))
+
+ n = int('BF 1E 27 90 0A A0 8B 23 51 1A 5C 12 81 AE 6D 93 31 2C 3E FE 91 3F 93 2E BE D4 92 F1 2D 16 B4 61 0C 32 8C B6 E2 08 AB 5F 45 AC BE 29 50 83 32 98 F3 12 2C 19 F7 84 92 DE DF 40 F0 E3 C1 90 33 85'.replace(" ",""),16)
+ e = 65537
+ d = int('09 44 83 12 9F 11 4D ED F6 7E DA BC 23 01 BC 5A 88 E5 E6 60 1D D7 01 62 20 EA D9 FD 4B FC 6F DE B7 58 93 89 8A E4 1C 54 DD BD BF 15 39 F8 CC BD 18 F6 7B 44 0D E1 AC 30 44 02 81 D4 0C FA C8 39'.replace(" ",""),16)
+ p = int('00 F2 0F 2F 3E 1D A6 18 83 F6 29 80 92 2B D8 DF 54 5C E4 07 C7 26 24 11 03 B5 E2 C5 37 23 12 4A 23'.replace(" ",""),16)
+ q = int('00 CA 1F E9 24 79 2C FC C9 6B FA B7 4F 34 4A 68 B4 18 DF 57 83 38 06 48 06 00 0F E2 A5 C9 9A 02 37'.replace(" ",""),16)
+
+ # This is q^{-1} mod p). fastmath and slowmath use pInv (p^{-1}
+ # mod q) instead!
+ qInv = int('00 BD 9F 40 A7 64 22 7A 21 96 2A 4A DD 07 E4 DE FE 43 ED 91 A3 AE 27 BB 05 7F 39 24 1F 33 AB 01 C1'.replace(" ",""),16)
+ pInv = inverse(p,q)
+
+ def testImportKey1(self):
+ """Verify import of RSAPrivateKey DER SEQUENCE"""
+ key = RSA.importKey(self.rsaKeyDER)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+ self.assertEqual(key.d, self.d)
+ self.assertEqual(key.p, self.p)
+ self.assertEqual(key.q, self.q)
+
+ def testImportKey2(self):
+ """Verify import of SubjectPublicKeyInfo DER SEQUENCE"""
+ key = RSA.importKey(self.rsaPublicKeyDER)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+
+ def testImportKey3unicode(self):
+ """Verify import of RSAPrivateKey DER SEQUENCE, encoded with PEM as unicode"""
+ key = RSA.importKey(self.rsaKeyPEM)
+ self.assertEqual(key.has_private(),True) # assert_
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+ self.assertEqual(key.d, self.d)
+ self.assertEqual(key.p, self.p)
+ self.assertEqual(key.q, self.q)
+
+ def testImportKey3bytes(self):
+ """Verify import of RSAPrivateKey DER SEQUENCE, encoded with PEM as byte string"""
+ key = RSA.importKey(b(self.rsaKeyPEM))
+ self.assertEqual(key.has_private(),True) # assert_
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+ self.assertEqual(key.d, self.d)
+ self.assertEqual(key.p, self.p)
+ self.assertEqual(key.q, self.q)
+
+ def testImportKey4unicode(self):
+ """Verify import of RSAPrivateKey DER SEQUENCE, encoded with PEM as unicode"""
+ key = RSA.importKey(self.rsaPublicKeyPEM)
+ self.assertEqual(key.has_private(),False) # assertFalse
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+
+ def testImportKey4bytes(self):
+ """Verify import of SubjectPublicKeyInfo DER SEQUENCE, encoded with PEM as byte string"""
+ key = RSA.importKey(b(self.rsaPublicKeyPEM))
+ self.assertEqual(key.has_private(),False) # assertFalse
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+
+ def testImportKey5(self):
+ """Verifies that the imported key is still a valid RSA pair"""
+ key = RSA.importKey(self.rsaKeyPEM)
+ idem = key._encrypt(key._decrypt(89))
+ self.assertEqual(idem, 89)
+
+ def testImportKey6(self):
+ """Verifies that the imported key is still a valid RSA pair"""
+ key = RSA.importKey(self.rsaKeyDER)
+ idem = key._encrypt(key._decrypt(65))
+ self.assertEqual(idem, 65)
+
+ def testImportKey7(self):
+ """Verify import of OpenSSH public key"""
+ key = RSA.importKey(self.rsaPublicKeyOpenSSH)
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+
+ def testImportKey8(self):
+ """Verify import of encrypted PrivateKeyInfo DER SEQUENCE"""
+ for t in self.rsaKeyEncryptedPEM:
+ key = RSA.importKey(t[1], t[0])
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+ self.assertEqual(key.d, self.d)
+ self.assertEqual(key.p, self.p)
+ self.assertEqual(key.q, self.q)
+
+ def testImportKey9(self):
+ """Verify import of unencrypted PrivateKeyInfo DER SEQUENCE"""
+ key = RSA.importKey(self.rsaKeyDER8)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+ self.assertEqual(key.d, self.d)
+ self.assertEqual(key.p, self.p)
+ self.assertEqual(key.q, self.q)
+
+ def testImportKey10(self):
+ """Verify import of unencrypted PrivateKeyInfo DER SEQUENCE, encoded with PEM"""
+ key = RSA.importKey(self.rsaKeyPEM8)
+ self.assertTrue(key.has_private())
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+ self.assertEqual(key.d, self.d)
+ self.assertEqual(key.p, self.p)
+ self.assertEqual(key.q, self.q)
+
+ def testImportKey11(self):
+ """Verify import of RSAPublicKey DER SEQUENCE"""
+ der = asn1.DerSequence([17, 3]).encode()
+ key = RSA.importKey(der)
+ self.assertEqual(key.n, 17)
+ self.assertEqual(key.e, 3)
+
+ def testImportKey12(self):
+ """Verify import of RSAPublicKey DER SEQUENCE, encoded with PEM"""
+ der = asn1.DerSequence([17, 3]).encode()
+ pem = der2pem(der)
+ key = RSA.importKey(pem)
+ self.assertEqual(key.n, 17)
+ self.assertEqual(key.e, 3)
+
+ def test_import_key_windows_cr_lf(self):
+ pem_cr_lf = "\r\n".join(self.rsaKeyPEM.splitlines())
+ key = RSA.importKey(pem_cr_lf)
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+ self.assertEqual(key.d, self.d)
+ self.assertEqual(key.p, self.p)
+ self.assertEqual(key.q, self.q)
+
+ def test_import_empty(self):
+ self.assertRaises(ValueError, RSA.import_key, b"")
+
+ ###
+ def testExportKey1(self):
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ derKey = key.export_key("DER")
+ self.assertEqual(derKey, self.rsaKeyDER)
+
+ def testExportKey2(self):
+ key = RSA.construct([self.n, self.e])
+ derKey = key.export_key("DER")
+ self.assertEqual(derKey, self.rsaPublicKeyDER)
+
+ def testExportKey3(self):
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ pemKey = key.export_key("PEM")
+ self.assertEqual(pemKey, b(self.rsaKeyPEM))
+
+ def testExportKey4(self):
+ key = RSA.construct([self.n, self.e])
+ pemKey = key.export_key("PEM")
+ self.assertEqual(pemKey, b(self.rsaPublicKeyPEM))
+
+ def testExportKey5(self):
+ key = RSA.construct([self.n, self.e])
+ openssh_1 = key.export_key("OpenSSH").split()
+ openssh_2 = self.rsaPublicKeyOpenSSH.split()
+ self.assertEqual(openssh_1[0], openssh_2[0])
+ self.assertEqual(openssh_1[1], openssh_2[1])
+
+ def testExportKey7(self):
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ derKey = key.export_key("DER", pkcs=8)
+ self.assertEqual(derKey, self.rsaKeyDER8)
+
+ def testExportKey8(self):
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ pemKey = key.export_key("PEM", pkcs=8)
+ self.assertEqual(pemKey, b(self.rsaKeyPEM8))
+
+ def testExportKey9(self):
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ self.assertRaises(ValueError, key.export_key, "invalid-format")
+
+ def testExportKey10(self):
+ # Export and re-import the encrypted key. It must match.
+ # PEM envelope, PKCS#1, old PEM encryption
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ outkey = key.export_key('PEM', 'test')
+ self.assertTrue(tostr(outkey).find('4,ENCRYPTED')!=-1)
+ self.assertTrue(tostr(outkey).find('BEGIN RSA PRIVATE KEY')!=-1)
+ inkey = RSA.importKey(outkey, 'test')
+ self.assertEqual(key.n, inkey.n)
+ self.assertEqual(key.e, inkey.e)
+ self.assertEqual(key.d, inkey.d)
+
+ def testExportKey11(self):
+ # Export and re-import the encrypted key. It must match.
+ # PEM envelope, PKCS#1, old PEM encryption
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ outkey = key.export_key('PEM', 'test', pkcs=1)
+ self.assertTrue(tostr(outkey).find('4,ENCRYPTED')!=-1)
+ self.assertTrue(tostr(outkey).find('BEGIN RSA PRIVATE KEY')!=-1)
+ inkey = RSA.importKey(outkey, 'test')
+ self.assertEqual(key.n, inkey.n)
+ self.assertEqual(key.e, inkey.e)
+ self.assertEqual(key.d, inkey.d)
+
+ def testExportKey12(self):
+ # Export and re-import the encrypted key. It must match.
+ # PEM envelope, PKCS#8, old PEM encryption
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ outkey = key.export_key('PEM', 'test', pkcs=8)
+ self.assertTrue(tostr(outkey).find('4,ENCRYPTED')!=-1)
+ self.assertTrue(tostr(outkey).find('BEGIN PRIVATE KEY')!=-1)
+ inkey = RSA.importKey(outkey, 'test')
+ self.assertEqual(key.n, inkey.n)
+ self.assertEqual(key.e, inkey.e)
+ self.assertEqual(key.d, inkey.d)
+
+ def testExportKey13(self):
+ # Export and re-import the encrypted key. It must match.
+ # PEM envelope, PKCS#8, PKCS#8 encryption
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ outkey = key.export_key('PEM', 'test', pkcs=8,
+ protection='PBKDF2WithHMAC-SHA1AndDES-EDE3-CBC')
+ self.assertTrue(tostr(outkey).find('4,ENCRYPTED')==-1)
+ self.assertTrue(tostr(outkey).find('BEGIN ENCRYPTED PRIVATE KEY')!=-1)
+ inkey = RSA.importKey(outkey, 'test')
+ self.assertEqual(key.n, inkey.n)
+ self.assertEqual(key.e, inkey.e)
+ self.assertEqual(key.d, inkey.d)
+
+ def testExportKey14(self):
+ # Export and re-import the encrypted key. It must match.
+ # DER envelope, PKCS#8, PKCS#8 encryption
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ outkey = key.export_key('DER', 'test', pkcs=8)
+ inkey = RSA.importKey(outkey, 'test')
+ self.assertEqual(key.n, inkey.n)
+ self.assertEqual(key.e, inkey.e)
+ self.assertEqual(key.d, inkey.d)
+
+ def testExportKey15(self):
+ # Verify that that error an condition is detected when trying to
+ # use a password with DER encoding and PKCS#1.
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ self.assertRaises(ValueError, key.export_key, 'DER', 'test', 1)
+
+ def testExportKey16(self):
+ # Export and re-import the encrypted key. It must match.
+ # PEM envelope, PKCS#8, PKCS#8 encryption with parameters
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ outkey = key.export_key('PEM', 'test', pkcs=8,
+ protection='PBKDF2WithHMAC-SHA512AndAES256-CBC',
+ prot_params={'iteration_count':123}
+ )
+ self.assertTrue(tostr(outkey).find('4,ENCRYPTED')==-1)
+ self.assertTrue(tostr(outkey).find('BEGIN ENCRYPTED PRIVATE KEY')!=-1)
+
+ # Verify the iteration count
+ der = PEM.decode(tostr(outkey))[0]
+ seq1 = asn1.DerSequence().decode(der)
+ seq2 = asn1.DerSequence().decode(seq1[0])
+ seq3 = asn1.DerSequence().decode(seq2[1])
+ seq4 = asn1.DerSequence().decode(seq3[0])
+ seq5 = asn1.DerSequence().decode(seq4[1])
+ self.assertEqual(seq5[1], 123)
+
+ inkey = RSA.importKey(outkey, 'test')
+ self.assertEqual(key.n, inkey.n)
+ self.assertEqual(key.e, inkey.e)
+ self.assertEqual(key.d, inkey.d)
+
+ def test_import_key(self):
+ """Verify that import_key is an alias to importKey"""
+ key = RSA.import_key(self.rsaPublicKeyDER)
+ self.assertFalse(key.has_private())
+ self.assertEqual(key.n, self.n)
+ self.assertEqual(key.e, self.e)
+
+ def test_import_key_ba_mv(self):
+ """Verify that import_key can be used on bytearrays and memoryviews"""
+ key = RSA.import_key(bytearray(self.rsaPublicKeyDER))
+ key = RSA.import_key(memoryview(self.rsaPublicKeyDER))
+
+ def test_exportKey(self):
+ key = RSA.construct([self.n, self.e, self.d, self.p, self.q, self.pInv])
+ self.assertEqual(key.export_key(), key.exportKey())
+
+
+class ImportKeyFromX509Cert(unittest.TestCase):
+
+ def test_x509v1(self):
+
+ # Sample V1 certificate with a 1024 bit RSA key
+ x509_v1_cert = """
+-----BEGIN CERTIFICATE-----
+MIICOjCCAaMCAQEwDQYJKoZIhvcNAQEEBQAwfjENMAsGA1UEChMEQWNtZTELMAkG
+A1UECxMCUkQxHDAaBgkqhkiG9w0BCQEWDXNwYW1AYWNtZS5vcmcxEzARBgNVBAcT
+Ck1ldHJvcG9saXMxETAPBgNVBAgTCE5ldyBZb3JrMQswCQYDVQQGEwJVUzENMAsG
+A1UEAxMEdGVzdDAeFw0xNDA3MTExOTU3MjRaFw0xNzA0MDYxOTU3MjRaME0xCzAJ
+BgNVBAYTAlVTMREwDwYDVQQIEwhOZXcgWW9yazENMAsGA1UEChMEQWNtZTELMAkG
+A1UECxMCUkQxDzANBgNVBAMTBmxhdHZpYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEAyG+kytdRj3TFbRmHDYp3TXugVQ81chew0qeOxZWOz80IjtWpgdOaCvKW
+NCuc8wUR9BWrEQW+39SaRMLiQfQtyFSQZijc3nsEBu/Lo4uWZ0W/FHDRVSvkJA/V
+Ex5NL5ikI+wbUeCV5KajGNDalZ8F1pk32+CBs8h1xNx5DyxuEHUCAwEAATANBgkq
+hkiG9w0BAQQFAAOBgQCVQF9Y//Q4Psy+umEM38pIlbZ2hxC5xNz/MbVPwuCkNcGn
+KYNpQJP+JyVTsPpO8RLZsAQDzRueMI3S7fbbwTzAflN0z19wvblvu93xkaBytVok
+9VBAH28olVhy9b1MMeg2WOt5sUEQaFNPnwwsyiY9+HsRpvpRnPSQF+kyYVsshQ==
+-----END CERTIFICATE-----
+ """.strip()
+
+ # RSA public key as dumped by openssl
+ exponent = 65537
+ modulus_str = """
+00:c8:6f:a4:ca:d7:51:8f:74:c5:6d:19:87:0d:8a:
+77:4d:7b:a0:55:0f:35:72:17:b0:d2:a7:8e:c5:95:
+8e:cf:cd:08:8e:d5:a9:81:d3:9a:0a:f2:96:34:2b:
+9c:f3:05:11:f4:15:ab:11:05:be:df:d4:9a:44:c2:
+e2:41:f4:2d:c8:54:90:66:28:dc:de:7b:04:06:ef:
+cb:a3:8b:96:67:45:bf:14:70:d1:55:2b:e4:24:0f:
+d5:13:1e:4d:2f:98:a4:23:ec:1b:51:e0:95:e4:a6:
+a3:18:d0:da:95:9f:05:d6:99:37:db:e0:81:b3:c8:
+75:c4:dc:79:0f:2c:6e:10:75
+ """
+ modulus = int(re.sub("[^0-9a-f]","", modulus_str), 16)
+
+ key = RSA.importKey(x509_v1_cert)
+ self.assertEqual(key.e, exponent)
+ self.assertEqual(key.n, modulus)
+ self.assertFalse(key.has_private())
+
+ def test_x509v3(self):
+
+ # Sample V3 certificate with a 1024 bit RSA key
+ x509_v3_cert = """
+-----BEGIN CERTIFICATE-----
+MIIEcjCCAlqgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJVUzEL
+MAkGA1UECAwCTUQxEjAQBgNVBAcMCUJhbHRpbW9yZTEQMA4GA1UEAwwHVGVzdCBD
+QTEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTAeFw0xNDA3MTIwOTM1
+MTJaFw0xNzA0MDcwOTM1MTJaMEQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJNRDES
+MBAGA1UEBwwJQmFsdGltb3JlMRQwEgYDVQQDDAtUZXN0IFNlcnZlcjCBnzANBgkq
+hkiG9w0BAQEFAAOBjQAwgYkCgYEA/S7GJV2OcFdyNMQ4K75KrYFtMEn3VnEFdPHa
+jyS37XlMxSh0oS4GeTGVUCJInl5Cpsv8WQdh03FfeOdvzp5IZ46OcjeOPiWnmjgl
+2G5j7e2bDH7RSchGV+OD6Fb1Agvuu2/9iy8fdf3rPQ/7eAddzKUrzwacVbnW+tg2
+QtSXKRcCAwEAAaOB1TCB0jAdBgNVHQ4EFgQU/WwCX7FfWMIPDFfJ+I8a2COG+l8w
+HwYDVR0jBBgwFoAUa0hkif3RMaraiWtsOOZZlLu9wJwwCQYDVR0TBAIwADALBgNV
+HQ8EBAMCBeAwSgYDVR0RBEMwQYILZXhhbXBsZS5jb22CD3d3dy5leGFtcGxlLmNv
+bYIQbWFpbC5leGFtcGxlLmNvbYIPZnRwLmV4YW1wbGUuY29tMCwGCWCGSAGG+EIB
+DQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTANBgkqhkiG9w0BAQsF
+AAOCAgEAvO6xfdsGbnoK4My3eJthodTAjMjPwFVY133LH04QLcCv54TxKhtUg1fi
+PgdjVe1HpTytPBfXy2bSZbXAN0abZCtw1rYrnn7o1g2pN8iypVq3zVn0iMTzQzxs
+zEPO3bpR/UhNSf90PmCsS5rqZpAAnXSaAy1ClwHWk/0eG2pYkhE1m1ABVMN2lsAW
+e9WxGk6IFqaI9O37NYQwmEypMs4DC+ECJEvbPFiqi3n0gbXCZJJ6omDA5xJldaYK
+Oa7KR3s/qjBsu9UAiWpLBuFoSTHIF2aeRKRFmUdmzwo43eVPep65pY6eQ4AdL2RF
+rqEuINbGlzI5oQyYhu71IwB+iPZXaZZPlwjLgOsuad/p2hOgDb5WxUi8FnDPursQ
+ujfpIpmrOP/zpvvQWnwePI3lI+5n41kTBSbefXEdv6rXpHk3QRzB90uPxnXPdxSC
+16ASA8bQT5an/1AgoE3k9CrcD2K0EmgaX0YI0HUhkyzbkg34EhpWJ6vvRUbRiNRo
+9cIbt/ya9Y9u0Ja8GLXv6dwX0l0IdJMkL8KifXUFAVCujp1FBrr/gdmwQn8itANy
++qbnWSxmOvtaY0zcaFAcONuHva0h51/WqXOMO1eb8PhR4HIIYU8p1oBwQp7dSni8
+THDi1F+GG5PsymMDj5cWK42f+QzjVw5PrVmFqqrrEoMlx8DWh5Y=
+-----END CERTIFICATE-----
+""".strip()
+
+ # RSA public key as dumped by openssl
+ exponent = 65537
+ modulus_str = """
+00:fd:2e:c6:25:5d:8e:70:57:72:34:c4:38:2b:be:
+4a:ad:81:6d:30:49:f7:56:71:05:74:f1:da:8f:24:
+b7:ed:79:4c:c5:28:74:a1:2e:06:79:31:95:50:22:
+48:9e:5e:42:a6:cb:fc:59:07:61:d3:71:5f:78:e7:
+6f:ce:9e:48:67:8e:8e:72:37:8e:3e:25:a7:9a:38:
+25:d8:6e:63:ed:ed:9b:0c:7e:d1:49:c8:46:57:e3:
+83:e8:56:f5:02:0b:ee:bb:6f:fd:8b:2f:1f:75:fd:
+eb:3d:0f:fb:78:07:5d:cc:a5:2b:cf:06:9c:55:b9:
+d6:fa:d8:36:42:d4:97:29:17
+ """
+ modulus = int(re.sub("[^0-9a-f]","", modulus_str), 16)
+
+ key = RSA.importKey(x509_v3_cert)
+ self.assertEqual(key.e, exponent)
+ self.assertEqual(key.n, modulus)
+ self.assertFalse(key.has_private())
+
+
+class TestImport_2048(unittest.TestCase):
+
+ def test_import_pss(self):
+ pub_key_file = load_file("rsa2048_pss_public.pem")
+ pub_key = RSA.import_key(pub_key_file)
+
+ priv_key_file = load_file("rsa2048_pss_private.pem")
+ priv_key = RSA.import_key(priv_key_file)
+
+ self.assertEqual(pub_key.n, priv_key.n)
+
+ def test_import_openssh_public(self):
+ key_file_ref = load_file("rsa2048_private.pem")
+ key_file = load_file("rsa2048_public_openssh.txt")
+
+ # Skip test if test vectors are not installed
+ if None in (key_file_ref, key_file):
+ return
+
+ key_ref = RSA.import_key(key_file_ref).public_key()
+ key = RSA.import_key(key_file)
+ self.assertEqual(key_ref, key)
+
+ def test_import_openssh_private_clear(self):
+ key_file = load_file("rsa2048_private_openssh.pem")
+ key_file_old = load_file("rsa2048_private_openssh_old.pem")
+
+ # Skip test if test vectors are not installed
+ if None in (key_file_old, key_file):
+ return
+
+ key = RSA.import_key(key_file)
+ key_old = RSA.import_key(key_file_old)
+
+ self.assertEqual(key, key_old)
+
+ def test_import_openssh_private_password(self):
+ key_file = load_file("rsa2048_private_openssh_pwd.pem")
+ key_file_old = load_file("rsa2048_private_openssh_pwd_old.pem")
+
+ # Skip test if test vectors are not installed
+ if None in (key_file_old, key_file):
+ return
+
+ key = RSA.import_key(key_file, b"password")
+ key_old = RSA.import_key(key_file_old)
+ self.assertEqual(key, key_old)
+
+ def test_import_pkcs8_private(self):
+ key_file_ref = load_file("rsa2048_private.pem")
+ key_file = load_file("rsa2048_private_p8.der")
+
+ # Skip test if test vectors are not installed
+ if None in (key_file_ref, key_file):
+ return
+
+ key_ref = RSA.import_key(key_file_ref)
+ key = RSA.import_key(key_file, b'secret')
+ self.assertEqual(key_ref, key)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(ImportKeyTests)
+ tests += list_test_cases(ImportKeyFromX509Cert)
+ tests += list_test_cases(TestImport_2048)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Random/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Random/__init__.py
new file mode 100644
index 00000000..53061cc0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Random/__init__.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Random/__init__.py: Self-test for random number generation modules
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test for random number generators"""
+
+__revision__ = "$Id$"
+
+def get_tests(config={}):
+ tests = []
+ from Crypto.SelfTest.Random import test_random; tests += test_random.get_tests(config=config)
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Random/test_random.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Random/test_random.py
new file mode 100644
index 00000000..8fadc535
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Random/test_random.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Util/test_generic.py: Self-test for the Crypto.Random.new() function
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test suite for Crypto.Random.new()"""
+
+import sys
+import unittest
+from Crypto.Util.py3compat import b
+
+class SimpleTest(unittest.TestCase):
+ def runTest(self):
+ """Crypto.Random.new()"""
+ # Import the Random module and try to use it
+ from Crypto import Random
+ randobj = Random.new()
+ x = randobj.read(16)
+ y = randobj.read(16)
+ self.assertNotEqual(x, y)
+ z = Random.get_random_bytes(16)
+ self.assertNotEqual(x, z)
+ self.assertNotEqual(y, z)
+ # Test the Random.random module, which
+ # implements a subset of Python's random API
+ # Not implemented:
+ # seed(), getstate(), setstate(), jumpahead()
+ # random(), uniform(), triangular(), betavariate()
+ # expovariate(), gammavariate(), gauss(),
+ # longnormvariate(), normalvariate(),
+ # vonmisesvariate(), paretovariate()
+ # weibullvariate()
+ # WichmannHill(), whseed(), SystemRandom()
+ from Crypto.Random import random
+ x = random.getrandbits(16*8)
+ y = random.getrandbits(16*8)
+ self.assertNotEqual(x, y)
+ # Test randrange
+ if x>y:
+ start = y
+ stop = x
+ else:
+ start = x
+ stop = y
+ for step in range(1,10):
+ x = random.randrange(start,stop,step)
+ y = random.randrange(start,stop,step)
+ self.assertNotEqual(x, y)
+ self.assertEqual(start <= x < stop, True)
+ self.assertEqual(start <= y < stop, True)
+ self.assertEqual((x - start) % step, 0)
+ self.assertEqual((y - start) % step, 0)
+ for i in range(10):
+ self.assertEqual(random.randrange(1,2), 1)
+ self.assertRaises(ValueError, random.randrange, start, start)
+ self.assertRaises(ValueError, random.randrange, stop, start, step)
+ self.assertRaises(TypeError, random.randrange, start, stop, step, step)
+ self.assertRaises(TypeError, random.randrange, start, stop, "1")
+ self.assertRaises(TypeError, random.randrange, "1", stop, step)
+ self.assertRaises(TypeError, random.randrange, 1, "2", step)
+ self.assertRaises(ValueError, random.randrange, start, stop, 0)
+ # Test randint
+ x = random.randint(start,stop)
+ y = random.randint(start,stop)
+ self.assertNotEqual(x, y)
+ self.assertEqual(start <= x <= stop, True)
+ self.assertEqual(start <= y <= stop, True)
+ for i in range(10):
+ self.assertEqual(random.randint(1,1), 1)
+ self.assertRaises(ValueError, random.randint, stop, start)
+ self.assertRaises(TypeError, random.randint, start, stop, step)
+ self.assertRaises(TypeError, random.randint, "1", stop)
+ self.assertRaises(TypeError, random.randint, 1, "2")
+ # Test choice
+ seq = range(10000)
+ x = random.choice(seq)
+ y = random.choice(seq)
+ self.assertNotEqual(x, y)
+ self.assertEqual(x in seq, True)
+ self.assertEqual(y in seq, True)
+ for i in range(10):
+ self.assertEqual(random.choice((1,2,3)) in (1,2,3), True)
+ self.assertEqual(random.choice([1,2,3]) in [1,2,3], True)
+ if sys.version_info[0] == 3:
+ self.assertEqual(random.choice(bytearray(b('123'))) in bytearray(b('123')), True)
+ self.assertEqual(1, random.choice([1]))
+ self.assertRaises(IndexError, random.choice, [])
+ self.assertRaises(TypeError, random.choice, 1)
+ # Test shuffle. Lacks random parameter to specify function.
+ # Make copies of seq
+ seq = range(500)
+ x = list(seq)
+ y = list(seq)
+ random.shuffle(x)
+ random.shuffle(y)
+ self.assertNotEqual(x, y)
+ self.assertEqual(len(seq), len(x))
+ self.assertEqual(len(seq), len(y))
+ for i in range(len(seq)):
+ self.assertEqual(x[i] in seq, True)
+ self.assertEqual(y[i] in seq, True)
+ self.assertEqual(seq[i] in x, True)
+ self.assertEqual(seq[i] in y, True)
+ z = [1]
+ random.shuffle(z)
+ self.assertEqual(z, [1])
+ if sys.version_info[0] == 3:
+ z = bytearray(b('12'))
+ random.shuffle(z)
+ self.assertEqual(b('1') in z, True)
+ self.assertRaises(TypeError, random.shuffle, b('12'))
+ self.assertRaises(TypeError, random.shuffle, 1)
+ self.assertRaises(TypeError, random.shuffle, "11")
+ self.assertRaises(TypeError, random.shuffle, (1,2))
+ # 2to3 wraps a list() around it, alas - but I want to shoot
+ # myself in the foot here! :D
+ # if sys.version_info[0] == 3:
+ # self.assertRaises(TypeError, random.shuffle, range(3))
+ # Test sample
+ x = random.sample(seq, 20)
+ y = random.sample(seq, 20)
+ self.assertNotEqual(x, y)
+ for i in range(20):
+ self.assertEqual(x[i] in seq, True)
+ self.assertEqual(y[i] in seq, True)
+ z = random.sample([1], 1)
+ self.assertEqual(z, [1])
+ z = random.sample((1,2,3), 1)
+ self.assertEqual(z[0] in (1,2,3), True)
+ z = random.sample("123", 1)
+ self.assertEqual(z[0] in "123", True)
+ z = random.sample(range(3), 1)
+ self.assertEqual(z[0] in range(3), True)
+ if sys.version_info[0] == 3:
+ z = random.sample(b("123"), 1)
+ self.assertEqual(z[0] in b("123"), True)
+ z = random.sample(bytearray(b("123")), 1)
+ self.assertEqual(z[0] in bytearray(b("123")), True)
+ self.assertRaises(TypeError, random.sample, 1)
+
+def get_tests(config={}):
+ return [SimpleTest()]
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/__init__.py
new file mode 100644
index 00000000..83cf0f39
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/__init__.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Signature/__init__.py: Self-test for signature modules
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test for signature modules"""
+
+import unittest
+from . import test_pkcs1_15, test_pss, test_dss, test_eddsa
+
+
+def get_tests(config={}):
+ tests = []
+ tests += test_pkcs1_15.get_tests(config=config)
+ tests += test_pss.get_tests(config=config)
+ tests += test_dss.get_tests(config=config)
+ tests += test_eddsa.get_tests(config=config)
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_dss.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_dss.py
new file mode 100644
index 00000000..d3f8dfce
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_dss.py
@@ -0,0 +1,1369 @@
+#
+# SelfTest/Signature/test_dss.py: Self-test for DSS signatures
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 re
+import unittest
+from binascii import hexlify, unhexlify
+
+from Crypto.Util.py3compat import tobytes, bord, bchr
+
+from Crypto.Hash import (SHA1, SHA224, SHA256, SHA384, SHA512,
+ SHA3_224, SHA3_256, SHA3_384, SHA3_512)
+from Crypto.Signature import DSS
+from Crypto.PublicKey import DSA, ECC
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors, load_test_vectors_wycheproof
+from Crypto.Util.number import bytes_to_long, long_to_bytes
+
+
+def t2b(hexstring):
+ ws = hexstring.replace(" ", "").replace("\n", "")
+ return unhexlify(tobytes(ws))
+
+
+def t2l(hexstring):
+ ws = hexstring.replace(" ", "").replace("\n", "")
+ return int(ws, 16)
+
+
+def load_hash_by_name(hash_name):
+ return __import__("Crypto.Hash." + hash_name, globals(), locals(), ["new"])
+
+
+class StrRNG:
+
+ def __init__(self, randomness):
+ length = len(randomness)
+ self._idx = 0
+ # Fix required to get the right K (see how randint() works!)
+ self._randomness = long_to_bytes(bytes_to_long(randomness) - 1, length)
+
+ def __call__(self, n):
+ out = self._randomness[self._idx:self._idx + n]
+ self._idx += n
+ return out
+
+
+class FIPS_DSA_Tests(unittest.TestCase):
+
+ # 1st 1024 bit key from SigGen.txt
+ P = 0xa8f9cd201e5e35d892f85f80e4db2599a5676a3b1d4f190330ed3256b26d0e80a0e49a8fffaaad2a24f472d2573241d4d6d6c7480c80b4c67bb4479c15ada7ea8424d2502fa01472e760241713dab025ae1b02e1703a1435f62ddf4ee4c1b664066eb22f2e3bf28bb70a2a76e4fd5ebe2d1229681b5b06439ac9c7e9d8bde283
+ Q = 0xf85f0f83ac4df7ea0cdf8f469bfeeaea14156495
+ G = 0x2b3152ff6c62f14622b8f48e59f8af46883b38e79b8c74deeae9df131f8b856e3ad6c8455dab87cc0da8ac973417ce4f7878557d6cdf40b35b4a0ca3eb310c6a95d68ce284ad4e25ea28591611ee08b8444bd64b25f3f7c572410ddfb39cc728b9c936f85f419129869929cdb909a6a3a99bbe089216368171bd0ba81de4fe33
+ X = 0xc53eae6d45323164c7d07af5715703744a63fc3a
+ Y = 0x313fd9ebca91574e1c2eebe1517c57e0c21b0209872140c5328761bbb2450b33f1b18b409ce9ab7c4cd8fda3391e8e34868357c199e16a6b2eba06d6749def791d79e95d3a4d09b24c392ad89dbf100995ae19c01062056bb14bce005e8731efde175f95b975089bdcdaea562b32786d96f5a31aedf75364008ad4fffebb970b
+
+ key_pub = DSA.construct((Y, G, P, Q))
+ key_priv = DSA.construct((Y, G, P, Q, X))
+
+ def shortDescription(self):
+ return "FIPS DSA Tests"
+
+ def test_loopback(self):
+ hashed_msg = SHA512.new(b"test")
+ signer = DSS.new(self.key_priv, 'fips-186-3')
+ signature = signer.sign(hashed_msg)
+
+ verifier = DSS.new(self.key_pub, 'fips-186-3')
+ verifier.verify(hashed_msg, signature)
+
+ def test_negative_unapproved_hashes(self):
+ """Verify that unapproved hashes are rejected"""
+
+ from Crypto.Hash import RIPEMD160
+
+ self.description = "Unapproved hash (RIPEMD160) test"
+ hash_obj = RIPEMD160.new()
+ signer = DSS.new(self.key_priv, 'fips-186-3')
+ self.assertRaises(ValueError, signer.sign, hash_obj)
+ self.assertRaises(ValueError, signer.verify, hash_obj, b"\x00" * 40)
+
+ def test_negative_unknown_modes_encodings(self):
+ """Verify that unknown modes/encodings are rejected"""
+
+ self.description = "Unknown mode test"
+ self.assertRaises(ValueError, DSS.new, self.key_priv, 'fips-186-0')
+
+ self.description = "Unknown encoding test"
+ self.assertRaises(ValueError, DSS.new, self.key_priv, 'fips-186-3', 'xml')
+
+ def test_asn1_encoding(self):
+ """Verify ASN.1 encoding"""
+
+ self.description = "ASN.1 encoding test"
+ hash_obj = SHA1.new()
+ signer = DSS.new(self.key_priv, 'fips-186-3', 'der')
+ signature = signer.sign(hash_obj)
+
+ # Verify that output looks like a DER SEQUENCE
+ self.assertEqual(bord(signature[0]), 48)
+ signer.verify(hash_obj, signature)
+
+ # Verify that ASN.1 parsing fails as expected
+ signature = bchr(7) + signature[1:]
+ self.assertRaises(ValueError, signer.verify, hash_obj, signature)
+
+ def test_sign_verify(self):
+ """Verify public/private method"""
+
+ self.description = "can_sign() test"
+ signer = DSS.new(self.key_priv, 'fips-186-3')
+ self.assertTrue(signer.can_sign())
+
+ signer = DSS.new(self.key_pub, 'fips-186-3')
+ self.assertFalse(signer.can_sign())
+
+ try:
+ signer.sign(SHA256.new(b'xyz'))
+ except TypeError as e:
+ msg = str(e)
+ else:
+ msg = ""
+ self.assertTrue("Private key is needed" in msg)
+
+
+class FIPS_DSA_Tests_KAT(unittest.TestCase):
+ pass
+
+
+test_vectors_verify = load_test_vectors(("Signature", "DSA"),
+ "FIPS_186_3_SigVer.rsp",
+ "Signature Verification 186-3",
+ {'result': lambda x: x}) or []
+
+for idx, tv in enumerate(test_vectors_verify):
+
+ if isinstance(tv, str):
+ res = re.match(r"\[mod = L=([0-9]+), N=([0-9]+), ([a-zA-Z0-9-]+)\]", tv)
+ assert(res)
+ hash_name = res.group(3).replace("-", "")
+ hash_module = load_hash_by_name(hash_name)
+ continue
+
+ if hasattr(tv, "p"):
+ modulus = tv.p
+ generator = tv.g
+ suborder = tv.q
+ continue
+
+ hash_obj = hash_module.new(tv.msg)
+
+ comps = [bytes_to_long(x) for x in (tv.y, generator, modulus, suborder)]
+ key = DSA.construct(comps, False) # type: ignore
+ verifier = DSS.new(key, 'fips-186-3')
+
+ def positive_test(self, verifier=verifier, hash_obj=hash_obj, signature=tv.r+tv.s):
+ verifier.verify(hash_obj, signature)
+
+ def negative_test(self, verifier=verifier, hash_obj=hash_obj, signature=tv.r+tv.s):
+ self.assertRaises(ValueError, verifier.verify, hash_obj, signature)
+
+ if tv.result == 'p':
+ setattr(FIPS_DSA_Tests_KAT, "test_verify_positive_%d" % idx, positive_test)
+ else:
+ setattr(FIPS_DSA_Tests_KAT, "test_verify_negative_%d" % idx, negative_test)
+
+
+test_vectors_sign = load_test_vectors(("Signature", "DSA"),
+ "FIPS_186_3_SigGen.txt",
+ "Signature Creation 186-3",
+ {}) or []
+
+for idx, tv in enumerate(test_vectors_sign):
+
+ if isinstance(tv, str):
+ res = re.match(r"\[mod = L=([0-9]+), N=([0-9]+), ([a-zA-Z0-9-]+)\]", tv)
+ assert(res)
+ hash_name = res.group(3).replace("-", "")
+ hash_module = load_hash_by_name(hash_name)
+ continue
+
+ if hasattr(tv, "p"):
+ modulus = tv.p
+ generator = tv.g
+ suborder = tv.q
+ continue
+
+ hash_obj = hash_module.new(tv.msg)
+ comps_dsa = [bytes_to_long(x) for x in (tv.y, generator, modulus, suborder, tv.x)]
+ key = DSA.construct(comps_dsa, False) # type: ignore
+ signer = DSS.new(key, 'fips-186-3', randfunc=StrRNG(tv.k))
+
+ def new_test(self, signer=signer, hash_obj=hash_obj, signature=tv.r+tv.s):
+ self.assertEqual(signer.sign(hash_obj), signature)
+ setattr(FIPS_DSA_Tests_KAT, "test_sign_%d" % idx, new_test)
+
+
+class FIPS_ECDSA_Tests(unittest.TestCase):
+
+ key_priv = ECC.generate(curve="P-256")
+ key_pub = key_priv.public_key()
+
+ def shortDescription(self):
+ return "FIPS ECDSA Tests"
+
+ def test_loopback(self):
+ hashed_msg = SHA512.new(b"test")
+ signer = DSS.new(self.key_priv, 'fips-186-3')
+ signature = signer.sign(hashed_msg)
+
+ verifier = DSS.new(self.key_pub, 'fips-186-3')
+ verifier.verify(hashed_msg, signature)
+
+ def test_negative_unapproved_hashes(self):
+ """Verify that unapproved hashes are rejected"""
+
+ from Crypto.Hash import SHA1
+
+ self.description = "Unapproved hash (SHA-1) test"
+ hash_obj = SHA1.new()
+ signer = DSS.new(self.key_priv, 'fips-186-3')
+ self.assertRaises(ValueError, signer.sign, hash_obj)
+ self.assertRaises(ValueError, signer.verify, hash_obj, b"\x00" * 40)
+
+ def test_negative_eddsa_key(self):
+ key = ECC.generate(curve="ed25519")
+ self.assertRaises(ValueError, DSS.new, key, 'fips-186-3')
+
+ def test_sign_verify(self):
+ """Verify public/private method"""
+
+ self.description = "can_sign() test"
+ signer = DSS.new(self.key_priv, 'fips-186-3')
+ self.assertTrue(signer.can_sign())
+
+ signer = DSS.new(self.key_pub, 'fips-186-3')
+ self.assertFalse(signer.can_sign())
+ self.assertRaises(TypeError, signer.sign, SHA256.new(b'xyz'))
+
+ try:
+ signer.sign(SHA256.new(b'xyz'))
+ except TypeError as e:
+ msg = str(e)
+ else:
+ msg = ""
+ self.assertTrue("Private key is needed" in msg)
+
+ def test_negative_unknown_modes_encodings(self):
+ """Verify that unknown modes/encodings are rejected"""
+
+ self.description = "Unknown mode test"
+ self.assertRaises(ValueError, DSS.new, self.key_priv, 'fips-186-0')
+
+ self.description = "Unknown encoding test"
+ self.assertRaises(ValueError, DSS.new, self.key_priv, 'fips-186-3', 'xml')
+
+ def test_asn1_encoding(self):
+ """Verify ASN.1 encoding"""
+
+ self.description = "ASN.1 encoding test"
+ hash_obj = SHA256.new()
+ signer = DSS.new(self.key_priv, 'fips-186-3', 'der')
+ signature = signer.sign(hash_obj)
+
+ # Verify that output looks like a DER SEQUENCE
+ self.assertEqual(bord(signature[0]), 48)
+ signer.verify(hash_obj, signature)
+
+ # Verify that ASN.1 parsing fails as expected
+ signature = bchr(7) + signature[1:]
+ self.assertRaises(ValueError, signer.verify, hash_obj, signature)
+
+
+class FIPS_ECDSA_Tests_KAT(unittest.TestCase):
+ pass
+
+
+test_vectors_verify = load_test_vectors(("Signature", "ECDSA"),
+ "SigVer.rsp",
+ "ECDSA Signature Verification 186-3",
+ {'result': lambda x: x,
+ 'qx': lambda x: int(x, 16),
+ 'qy': lambda x: int(x, 16),
+ }) or []
+test_vectors_verify += load_test_vectors(("Signature", "ECDSA"),
+ "SigVer_TruncatedSHAs.rsp",
+ "ECDSA Signature Verification 186-3",
+ {'result': lambda x: x,
+ 'qx': lambda x: int(x, 16),
+ 'qy': lambda x: int(x, 16),
+ }) or []
+
+
+for idx, tv in enumerate(test_vectors_verify):
+
+ if isinstance(tv, str):
+ res = re.match(r"\[(P-[0-9]+),(SHA-[0-9]+)\]", tv)
+ assert res
+ curve_name = res.group(1)
+ hash_name = res.group(2).replace("-", "")
+ if hash_name in ("SHA512224", "SHA512256"):
+ truncate = hash_name[-3:]
+ hash_name = hash_name[:-3]
+ else:
+ truncate = None
+ hash_module = load_hash_by_name(hash_name)
+ continue
+
+ if truncate is None:
+ hash_obj = hash_module.new(tv.msg)
+ else:
+ hash_obj = hash_module.new(tv.msg, truncate=truncate)
+ ecc_key = ECC.construct(curve=curve_name, point_x=tv.qx, point_y=tv.qy)
+ verifier = DSS.new(ecc_key, 'fips-186-3')
+
+ def positive_test(self, verifier=verifier, hash_obj=hash_obj, signature=tv.r+tv.s):
+ verifier.verify(hash_obj, signature)
+
+ def negative_test(self, verifier=verifier, hash_obj=hash_obj, signature=tv.r+tv.s):
+ self.assertRaises(ValueError, verifier.verify, hash_obj, signature)
+
+ if tv.result.startswith('p'):
+ setattr(FIPS_ECDSA_Tests_KAT, "test_verify_positive_%d" % idx, positive_test)
+ else:
+ setattr(FIPS_ECDSA_Tests_KAT, "test_verify_negative_%d" % idx, negative_test)
+
+
+test_vectors_sign = load_test_vectors(("Signature", "ECDSA"),
+ "SigGen.txt",
+ "ECDSA Signature Verification 186-3",
+ {'d': lambda x: int(x, 16)}) or []
+
+for idx, tv in enumerate(test_vectors_sign):
+
+ if isinstance(tv, str):
+ res = re.match(r"\[(P-[0-9]+),(SHA-[0-9]+)\]", tv)
+ assert res
+ curve_name = res.group(1)
+ hash_name = res.group(2).replace("-", "")
+ hash_module = load_hash_by_name(hash_name)
+ continue
+
+ hash_obj = hash_module.new(tv.msg)
+ ecc_key = ECC.construct(curve=curve_name, d=tv.d)
+ signer = DSS.new(ecc_key, 'fips-186-3', randfunc=StrRNG(tv.k))
+
+ def sign_test(self, signer=signer, hash_obj=hash_obj, signature=tv.r+tv.s):
+ self.assertEqual(signer.sign(hash_obj), signature)
+ setattr(FIPS_ECDSA_Tests_KAT, "test_sign_%d" % idx, sign_test)
+
+
+class Det_DSA_Tests(unittest.TestCase):
+ """Tests from rfc6979"""
+
+ # Each key is (p, q, g, x, y, desc)
+ keys = [
+ (
+ """
+ 86F5CA03DCFEB225063FF830A0C769B9DD9D6153AD91D7CE27F787C43278B447
+ E6533B86B18BED6E8A48B784A14C252C5BE0DBF60B86D6385BD2F12FB763ED88
+ 73ABFD3F5BA2E0A8C0A59082EAC056935E529DAF7C610467899C77ADEDFC846C
+ 881870B7B19B2B58F9BE0521A17002E3BDD6B86685EE90B3D9A1B02B782B1779""",
+ "996F967F6C8E388D9E28D01E205FBA957A5698B1",
+ """
+ 07B0F92546150B62514BB771E2A0C0CE387F03BDA6C56B505209FF25FD3C133D
+ 89BBCD97E904E09114D9A7DEFDEADFC9078EA544D2E401AEECC40BB9FBBF78FD
+ 87995A10A1C27CB7789B594BA7EFB5C4326A9FE59A070E136DB77175464ADCA4
+ 17BE5DCE2F40D10A46A3A3943F26AB7FD9C0398FF8C76EE0A56826A8A88F1DBD""",
+ "411602CB19A6CCC34494D79D98EF1E7ED5AF25F7",
+ """
+ 5DF5E01DED31D0297E274E1691C192FE5868FEF9E19A84776454B100CF16F653
+ 92195A38B90523E2542EE61871C0440CB87C322FC4B4D2EC5E1E7EC766E1BE8D
+ 4CE935437DC11C3C8FD426338933EBFE739CB3465F4D3668C5E473508253B1E6
+ 82F65CBDC4FAE93C2EA212390E54905A86E2223170B44EAA7DA5DD9FFCFB7F3B""",
+ "DSA1024"
+ ),
+ (
+ """
+ 9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642F0B5C48
+ C8F7A41AADFA187324B87674FA1822B00F1ECF8136943D7C55757264E5A1A44F
+ FE012E9936E00C1D3E9310B01C7D179805D3058B2A9F4BB6F9716BFE6117C6B5
+ B3CC4D9BE341104AD4A80AD6C94E005F4B993E14F091EB51743BF33050C38DE2
+ 35567E1B34C3D6A5C0CEAA1A0F368213C3D19843D0B4B09DCB9FC72D39C8DE41
+ F1BF14D4BB4563CA28371621CAD3324B6A2D392145BEBFAC748805236F5CA2FE
+ 92B871CD8F9C36D3292B5509CA8CAA77A2ADFC7BFD77DDA6F71125A7456FEA15
+ 3E433256A2261C6A06ED3693797E7995FAD5AABBCFBE3EDA2741E375404AE25B""",
+ "F2C3119374CE76C9356990B465374A17F23F9ED35089BD969F61C6DDE9998C1F",
+ """
+ 5C7FF6B06F8F143FE8288433493E4769C4D988ACE5BE25A0E24809670716C613
+ D7B0CEE6932F8FAA7C44D2CB24523DA53FBE4F6EC3595892D1AA58C4328A06C4
+ 6A15662E7EAA703A1DECF8BBB2D05DBE2EB956C142A338661D10461C0D135472
+ 085057F3494309FFA73C611F78B32ADBB5740C361C9F35BE90997DB2014E2EF5
+ AA61782F52ABEB8BD6432C4DD097BC5423B285DAFB60DC364E8161F4A2A35ACA
+ 3A10B1C4D203CC76A470A33AFDCBDD92959859ABD8B56E1725252D78EAC66E71
+ BA9AE3F1DD2487199874393CD4D832186800654760E1E34C09E4D155179F9EC0
+ DC4473F996BDCE6EED1CABED8B6F116F7AD9CF505DF0F998E34AB27514B0FFE7""",
+ "69C7548C21D0DFEA6B9A51C9EAD4E27C33D3B3F180316E5BCAB92C933F0E4DBC",
+ """
+ 667098C654426C78D7F8201EAC6C203EF030D43605032C2F1FA937E5237DBD94
+ 9F34A0A2564FE126DC8B715C5141802CE0979C8246463C40E6B6BDAA2513FA61
+ 1728716C2E4FD53BC95B89E69949D96512E873B9C8F8DFD499CC312882561ADE
+ CB31F658E934C0C197F2C4D96B05CBAD67381E7B768891E4DA3843D24D94CDFB
+ 5126E9B8BF21E8358EE0E0A30EF13FD6A664C0DCE3731F7FB49A4845A4FD8254
+ 687972A2D382599C9BAC4E0ED7998193078913032558134976410B89D2C171D1
+ 23AC35FD977219597AA7D15C1A9A428E59194F75C721EBCBCFAE44696A499AFA
+ 74E04299F132026601638CB87AB79190D4A0986315DA8EEC6561C938996BEADF""",
+ "DSA2048"
+ ),
+ ]
+
+ # This is a sequence of items:
+ # message, k, r, s, hash module
+ signatures = [
+ (
+ "sample",
+ "7BDB6B0FF756E1BB5D53583EF979082F9AD5BD5B",
+ "2E1A0C2562B2912CAAF89186FB0F42001585DA55",
+ "29EFB6B0AFF2D7A68EB70CA313022253B9A88DF5",
+ SHA1,
+ 'DSA1024'
+ ),
+ (
+ "sample",
+ "562097C06782D60C3037BA7BE104774344687649",
+ "4BC3B686AEA70145856814A6F1BB53346F02101E",
+ "410697B92295D994D21EDD2F4ADA85566F6F94C1",
+ SHA224,
+ 'DSA1024'
+ ),
+ (
+ "sample",
+ "519BA0546D0C39202A7D34D7DFA5E760B318BCFB",
+ "81F2F5850BE5BC123C43F71A3033E9384611C545",
+ "4CDD914B65EB6C66A8AAAD27299BEE6B035F5E89",
+ SHA256,
+ 'DSA1024'
+ ),
+ (
+ "sample",
+ "95897CD7BBB944AA932DBC579C1C09EB6FCFC595",
+ "07F2108557EE0E3921BC1774F1CA9B410B4CE65A",
+ "54DF70456C86FAC10FAB47C1949AB83F2C6F7595",
+ SHA384,
+ 'DSA1024'
+ ),
+ (
+ "sample",
+ "09ECE7CA27D0F5A4DD4E556C9DF1D21D28104F8B",
+ "16C3491F9B8C3FBBDD5E7A7B667057F0D8EE8E1B",
+ "02C36A127A7B89EDBB72E4FFBC71DABC7D4FC69C",
+ SHA512,
+ 'DSA1024'
+ ),
+ (
+ "test",
+ "5C842DF4F9E344EE09F056838B42C7A17F4A6433",
+ "42AB2052FD43E123F0607F115052A67DCD9C5C77",
+ "183916B0230D45B9931491D4C6B0BD2FB4AAF088",
+ SHA1,
+ 'DSA1024'
+ ),
+ (
+ "test",
+ "4598B8EFC1A53BC8AECD58D1ABBB0C0C71E67297",
+ "6868E9964E36C1689F6037F91F28D5F2C30610F2",
+ "49CEC3ACDC83018C5BD2674ECAAD35B8CD22940F",
+ SHA224,
+ 'DSA1024'
+ ),
+ (
+ "test",
+ "5A67592E8128E03A417B0484410FB72C0B630E1A",
+ "22518C127299B0F6FDC9872B282B9E70D0790812",
+ "6837EC18F150D55DE95B5E29BE7AF5D01E4FE160",
+ SHA256,
+ 'DSA1024'
+ ),
+ (
+ "test",
+ "220156B761F6CA5E6C9F1B9CF9C24BE25F98CD89",
+ "854CF929B58D73C3CBFDC421E8D5430CD6DB5E66",
+ "91D0E0F53E22F898D158380676A871A157CDA622",
+ SHA384,
+ 'DSA1024'
+ ),
+ (
+ "test",
+ "65D2C2EEB175E370F28C75BFCDC028D22C7DBE9C",
+ "8EA47E475BA8AC6F2D821DA3BD212D11A3DEB9A0",
+ "7C670C7AD72B6C050C109E1790008097125433E8",
+ SHA512,
+ 'DSA1024'
+ ),
+ (
+ "sample",
+ "888FA6F7738A41BDC9846466ABDB8174C0338250AE50CE955CA16230F9CBD53E",
+ "3A1B2DBD7489D6ED7E608FD036C83AF396E290DBD602408E8677DAABD6E7445A",
+ "D26FCBA19FA3E3058FFC02CA1596CDBB6E0D20CB37B06054F7E36DED0CDBBCCF",
+ SHA1,
+ 'DSA2048'
+ ),
+ (
+ "sample",
+ "BC372967702082E1AA4FCE892209F71AE4AD25A6DFD869334E6F153BD0C4D806",
+ "DC9F4DEADA8D8FF588E98FED0AB690FFCE858DC8C79376450EB6B76C24537E2C",
+ "A65A9C3BC7BABE286B195D5DA68616DA8D47FA0097F36DD19F517327DC848CEC",
+ SHA224,
+ 'DSA2048'
+ ),
+ (
+ "sample",
+ "8926A27C40484216F052F4427CFD5647338B7B3939BC6573AF4333569D597C52",
+ "EACE8BDBBE353C432A795D9EC556C6D021F7A03F42C36E9BC87E4AC7932CC809",
+ "7081E175455F9247B812B74583E9E94F9EA79BD640DC962533B0680793A38D53",
+ SHA256,
+ 'DSA2048'
+ ),
+ (
+ "sample",
+ "C345D5AB3DA0A5BCB7EC8F8FB7A7E96069E03B206371EF7D83E39068EC564920",
+ "B2DA945E91858834FD9BF616EBAC151EDBC4B45D27D0DD4A7F6A22739F45C00B",
+ "19048B63D9FD6BCA1D9BAE3664E1BCB97F7276C306130969F63F38FA8319021B",
+ SHA384,
+ 'DSA2048'
+ ),
+ (
+ "sample",
+ "5A12994431785485B3F5F067221517791B85A597B7A9436995C89ED0374668FC",
+ "2016ED092DC5FB669B8EFB3D1F31A91EECB199879BE0CF78F02BA062CB4C942E",
+ "D0C76F84B5F091E141572A639A4FB8C230807EEA7D55C8A154A224400AFF2351",
+ SHA512,
+ 'DSA2048'
+ ),
+ (
+ "test",
+ "6EEA486F9D41A037B2C640BC5645694FF8FF4B98D066A25F76BE641CCB24BA4F",
+ "C18270A93CFC6063F57A4DFA86024F700D980E4CF4E2CB65A504397273D98EA0",
+ "414F22E5F31A8B6D33295C7539C1C1BA3A6160D7D68D50AC0D3A5BEAC2884FAA",
+ SHA1,
+ 'DSA2048'
+ ),
+ (
+ "test",
+ "06BD4C05ED74719106223BE33F2D95DA6B3B541DAD7BFBD7AC508213B6DA6670",
+ "272ABA31572F6CC55E30BF616B7A265312018DD325BE031BE0CC82AA17870EA3",
+ "E9CC286A52CCE201586722D36D1E917EB96A4EBDB47932F9576AC645B3A60806",
+ SHA224,
+ 'DSA2048'
+ ),
+ (
+ "test",
+ "1D6CE6DDA1C5D37307839CD03AB0A5CBB18E60D800937D67DFB4479AAC8DEAD7",
+ "8190012A1969F9957D56FCCAAD223186F423398D58EF5B3CEFD5A4146A4476F0",
+ "7452A53F7075D417B4B013B278D1BB8BBD21863F5E7B1CEE679CF2188E1AB19E",
+ SHA256,
+ 'DSA2048'
+ ),
+ (
+ "test",
+ "206E61F73DBE1B2DC8BE736B22B079E9DACD974DB00EEBBC5B64CAD39CF9F91C",
+ "239E66DDBE8F8C230A3D071D601B6FFBDFB5901F94D444C6AF56F732BEB954BE",
+ "6BD737513D5E72FE85D1C750E0F73921FE299B945AAD1C802F15C26A43D34961",
+ SHA384,
+ 'DSA2048'
+ ),
+ (
+ "test",
+ "AFF1651E4CD6036D57AA8B2A05CCF1A9D5A40166340ECBBDC55BE10B568AA0AA",
+ "89EC4BB1400ECCFF8E7D9AA515CD1DE7803F2DAFF09693EE7FD1353E90A68307",
+ "C9F0BDABCC0D880BB137A994CC7F3980CE91CC10FAF529FC46565B15CEA854E1",
+ SHA512,
+ 'DSA2048'
+ )
+ ]
+
+ def setUp(self):
+ # Convert DSA key components from hex strings to integers
+ # Each key is (p, q, g, x, y, desc)
+
+ from collections import namedtuple
+
+ TestKey = namedtuple('TestKey', 'p q g x y')
+ new_keys = {}
+ for k in self.keys:
+ tk = TestKey(*[t2l(y) for y in k[:-1]])
+ new_keys[k[-1]] = tk
+ self.keys = new_keys
+
+ # Convert signature encoding
+ TestSig = namedtuple('TestSig', 'message nonce result module test_key')
+ new_signatures = []
+ for message, nonce, r, s, module, test_key in self.signatures:
+ tsig = TestSig(
+ tobytes(message),
+ t2l(nonce),
+ t2b(r) + t2b(s),
+ module,
+ self.keys[test_key]
+ )
+ new_signatures.append(tsig)
+ self.signatures = new_signatures
+
+ def test1(self):
+ q = 0x4000000000000000000020108A2E0CC0D99F8A5EF
+ x = 0x09A4D6792295A7F730FC3F2B49CBC0F62E862272F
+ p = 2 * q + 1
+ y = pow(2, x, p)
+ key = DSA.construct([pow(y, 2, p), 2, p, q, x], False)
+ signer = DSS.new(key, 'deterministic-rfc6979')
+
+ # Test _int2octets
+ self.assertEqual(hexlify(signer._int2octets(x)),
+ b'009a4d6792295a7f730fc3f2b49cbc0f62e862272f')
+
+ # Test _bits2octets
+ h1 = SHA256.new(b"sample").digest()
+ self.assertEqual(hexlify(signer._bits2octets(h1)),
+ b'01795edf0d54db760f156d0dac04c0322b3a204224')
+
+ def test2(self):
+
+ for sig in self.signatures:
+ tk = sig.test_key
+ key = DSA.construct([tk.y, tk.g, tk.p, tk.q, tk.x], False)
+ signer = DSS.new(key, 'deterministic-rfc6979')
+
+ hash_obj = sig.module.new(sig.message)
+ result = signer.sign(hash_obj)
+ self.assertEqual(sig.result, result)
+
+
+class Det_ECDSA_Tests(unittest.TestCase):
+
+ key_priv_p192 = ECC.construct(curve="P-192", d=0x6FAB034934E4C0FC9AE67F5B5659A9D7D1FEFD187EE09FD4)
+ key_pub_p192 = key_priv_p192.public_key()
+
+ key_priv_p224 = ECC.construct(curve="P-224", d=0xF220266E1105BFE3083E03EC7A3A654651F45E37167E88600BF257C1)
+ key_pub_p224 = key_priv_p224.public_key()
+
+ key_priv_p256 = ECC.construct(curve="P-256", d=0xC9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721)
+ key_pub_p256 = key_priv_p256.public_key()
+
+ key_priv_p384 = ECC.construct(curve="P-384", d=0x6B9D3DAD2E1B8C1C05B19875B6659F4DE23C3B667BF297BA9AA47740787137D896D5724E4C70A825F872C9EA60D2EDF5)
+ key_pub_p384 = key_priv_p384.public_key()
+
+ key_priv_p521 = ECC.construct(curve="P-521", d=0x0FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75CAA896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83538)
+ key_pub_p521 = key_priv_p521.public_key()
+
+ # This is a sequence of items:
+ # message, k, r, s, hash module
+ # taken from RFC6979
+ signatures_p192_ = (
+ (
+ "sample",
+ "37D7CA00D2C7B0E5E412AC03BD44BA837FDD5B28CD3B0021",
+ "98C6BD12B23EAF5E2A2045132086BE3EB8EBD62ABF6698FF",
+ "57A22B07DEA9530F8DE9471B1DC6624472E8E2844BC25B64",
+ SHA1
+ ),
+ (
+ "sample",
+ "4381526B3FC1E7128F202E194505592F01D5FF4C5AF015D8",
+ "A1F00DAD97AEEC91C95585F36200C65F3C01812AA60378F5",
+ "E07EC1304C7C6C9DEBBE980B9692668F81D4DE7922A0F97A",
+ SHA224
+ ),
+ (
+ "sample",
+ "32B1B6D7D42A05CB449065727A84804FB1A3E34D8F261496",
+ "4B0B8CE98A92866A2820E20AA6B75B56382E0F9BFD5ECB55",
+ "CCDB006926EA9565CBADC840829D8C384E06DE1F1E381B85",
+ SHA256
+ ),
+ (
+ "sample",
+ "4730005C4FCB01834C063A7B6760096DBE284B8252EF4311",
+ "DA63BF0B9ABCF948FBB1E9167F136145F7A20426DCC287D5",
+ "C3AA2C960972BD7A2003A57E1C4C77F0578F8AE95E31EC5E",
+ SHA384
+ ),
+ (
+ "sample",
+ "A2AC7AB055E4F20692D49209544C203A7D1F2C0BFBC75DB1",
+ "4D60C5AB1996BD848343B31C00850205E2EA6922DAC2E4B8",
+ "3F6E837448F027A1BF4B34E796E32A811CBB4050908D8F67",
+ SHA512
+ ),
+ (
+ "test",
+ "D9CF9C3D3297D3260773A1DA7418DB5537AB8DD93DE7FA25",
+ "0F2141A0EBBC44D2E1AF90A50EBCFCE5E197B3B7D4DE036D",
+ "EB18BC9E1F3D7387500CB99CF5F7C157070A8961E38700B7",
+ SHA1
+ ),
+ (
+ "test",
+ "F5DC805F76EF851800700CCE82E7B98D8911B7D510059FBE",
+ "6945A1C1D1B2206B8145548F633BB61CEF04891BAF26ED34",
+ "B7FB7FDFC339C0B9BD61A9F5A8EAF9BE58FC5CBA2CB15293",
+ SHA224
+ ),
+ (
+ "test",
+ "5C4CE89CF56D9E7C77C8585339B006B97B5F0680B4306C6C",
+ "3A718BD8B4926C3B52EE6BBE67EF79B18CB6EB62B1AD97AE",
+ "5662E6848A4A19B1F1AE2F72ACD4B8BBE50F1EAC65D9124F",
+ SHA256
+ ),
+ (
+ "test",
+ "5AFEFB5D3393261B828DB6C91FBC68C230727B030C975693",
+ "B234B60B4DB75A733E19280A7A6034BD6B1EE88AF5332367",
+ "7994090B2D59BB782BE57E74A44C9A1C700413F8ABEFE77A",
+ SHA384
+ ),
+ (
+ "test",
+ "0758753A5254759C7CFBAD2E2D9B0792EEE44136C9480527",
+ "FE4F4AE86A58B6507946715934FE2D8FF9D95B6B098FE739",
+ "74CF5605C98FBA0E1EF34D4B5A1577A7DCF59457CAE52290",
+ SHA512
+ )
+ )
+
+ signatures_p224_ = (
+ (
+ "sample",
+ "7EEFADD91110D8DE6C2C470831387C50D3357F7F4D477054B8B426BC",
+ "22226F9D40A96E19C4A301CE5B74B115303C0F3A4FD30FC257FB57AC",
+ "66D1CDD83E3AF75605DD6E2FEFF196D30AA7ED7A2EDF7AF475403D69",
+ SHA1
+ ),
+ (
+ "sample",
+ "C1D1F2F10881088301880506805FEB4825FE09ACB6816C36991AA06D",
+ "1CDFE6662DDE1E4A1EC4CDEDF6A1F5A2FB7FBD9145C12113E6ABFD3E",
+ "A6694FD7718A21053F225D3F46197CA699D45006C06F871808F43EBC",
+ SHA224
+ ),
+ (
+ "sample",
+ "AD3029E0278F80643DE33917CE6908C70A8FF50A411F06E41DEDFCDC",
+ "61AA3DA010E8E8406C656BC477A7A7189895E7E840CDFE8FF42307BA",
+ "BC814050DAB5D23770879494F9E0A680DC1AF7161991BDE692B10101",
+ SHA256
+ ),
+ (
+ "sample",
+ "52B40F5A9D3D13040F494E83D3906C6079F29981035C7BD51E5CAC40",
+ "0B115E5E36F0F9EC81F1325A5952878D745E19D7BB3EABFABA77E953",
+ "830F34CCDFE826CCFDC81EB4129772E20E122348A2BBD889A1B1AF1D",
+ SHA384
+ ),
+ (
+ "sample",
+ "9DB103FFEDEDF9CFDBA05184F925400C1653B8501BAB89CEA0FBEC14",
+ "074BD1D979D5F32BF958DDC61E4FB4872ADCAFEB2256497CDAC30397",
+ "A4CECA196C3D5A1FF31027B33185DC8EE43F288B21AB342E5D8EB084",
+ SHA512
+ ),
+ (
+ "test",
+ "2519178F82C3F0E4F87ED5883A4E114E5B7A6E374043D8EFD329C253",
+ "DEAA646EC2AF2EA8AD53ED66B2E2DDAA49A12EFD8356561451F3E21C",
+ "95987796F6CF2062AB8135271DE56AE55366C045F6D9593F53787BD2",
+ SHA1
+ ),
+ (
+ "test",
+ "DF8B38D40DCA3E077D0AC520BF56B6D565134D9B5F2EAE0D34900524",
+ "C441CE8E261DED634E4CF84910E4C5D1D22C5CF3B732BB204DBEF019",
+ "902F42847A63BDC5F6046ADA114953120F99442D76510150F372A3F4",
+ SHA224
+ ),
+ (
+ "test",
+ "FF86F57924DA248D6E44E8154EB69F0AE2AEBAEE9931D0B5A969F904",
+ "AD04DDE87B84747A243A631EA47A1BA6D1FAA059149AD2440DE6FBA6",
+ "178D49B1AE90E3D8B629BE3DB5683915F4E8C99FDF6E666CF37ADCFD",
+ SHA256
+ ),
+ (
+ "test",
+ "7046742B839478C1B5BD31DB2E862AD868E1A45C863585B5F22BDC2D",
+ "389B92682E399B26518A95506B52C03BC9379A9DADF3391A21FB0EA4",
+ "414A718ED3249FF6DBC5B50C27F71F01F070944DA22AB1F78F559AAB",
+ SHA384
+ ),
+ (
+ "test",
+ "E39C2AA4EA6BE2306C72126D40ED77BF9739BB4D6EF2BBB1DCB6169D",
+ "049F050477C5ADD858CAC56208394B5A55BAEBBE887FDF765047C17C",
+ "077EB13E7005929CEFA3CD0403C7CDCC077ADF4E44F3C41B2F60ECFF",
+ SHA512
+ )
+ )
+
+ signatures_p256_ = (
+ (
+ "sample",
+ "882905F1227FD620FBF2ABF21244F0BA83D0DC3A9103DBBEE43A1FB858109DB4",
+ "61340C88C3AAEBEB4F6D667F672CA9759A6CCAA9FA8811313039EE4A35471D32",
+ "6D7F147DAC089441BB2E2FE8F7A3FA264B9C475098FDCF6E00D7C996E1B8B7EB",
+ SHA1
+ ),
+ (
+ "sample",
+ "103F90EE9DC52E5E7FB5132B7033C63066D194321491862059967C715985D473",
+ "53B2FFF5D1752B2C689DF257C04C40A587FABABB3F6FC2702F1343AF7CA9AA3F",
+ "B9AFB64FDC03DC1A131C7D2386D11E349F070AA432A4ACC918BEA988BF75C74C",
+ SHA224
+ ),
+ (
+ "sample",
+ "A6E3C57DD01ABE90086538398355DD4C3B17AA873382B0F24D6129493D8AAD60",
+ "EFD48B2AACB6A8FD1140DD9CD45E81D69D2C877B56AAF991C34D0EA84EAF3716",
+ "F7CB1C942D657C41D436C7A1B6E29F65F3E900DBB9AFF4064DC4AB2F843ACDA8",
+ SHA256
+ ),
+ (
+ "sample",
+ "09F634B188CEFD98E7EC88B1AA9852D734D0BC272F7D2A47DECC6EBEB375AAD4",
+ "0EAFEA039B20E9B42309FB1D89E213057CBF973DC0CFC8F129EDDDC800EF7719",
+ "4861F0491E6998B9455193E34E7B0D284DDD7149A74B95B9261F13ABDE940954",
+ SHA384
+ ),
+ (
+ "sample",
+ "5FA81C63109BADB88C1F367B47DA606DA28CAD69AA22C4FE6AD7DF73A7173AA5",
+ "8496A60B5E9B47C825488827E0495B0E3FA109EC4568FD3F8D1097678EB97F00",
+ "2362AB1ADBE2B8ADF9CB9EDAB740EA6049C028114F2460F96554F61FAE3302FE",
+ SHA512
+ ),
+ (
+ "test",
+ "8C9520267C55D6B980DF741E56B4ADEE114D84FBFA2E62137954164028632A2E",
+ "0CBCC86FD6ABD1D99E703E1EC50069EE5C0B4BA4B9AC60E409E8EC5910D81A89",
+ "01B9D7B73DFAA60D5651EC4591A0136F87653E0FD780C3B1BC872FFDEAE479B1",
+ SHA1
+ ),
+ (
+ "test",
+ "669F4426F2688B8BE0DB3A6BD1989BDAEFFF84B649EEB84F3DD26080F667FAA7",
+ "C37EDB6F0AE79D47C3C27E962FA269BB4F441770357E114EE511F662EC34A692",
+ "C820053A05791E521FCAAD6042D40AEA1D6B1A540138558F47D0719800E18F2D",
+ SHA224
+ ),
+ (
+ "test",
+ "D16B6AE827F17175E040871A1C7EC3500192C4C92677336EC2537ACAEE0008E0",
+ "F1ABB023518351CD71D881567B1EA663ED3EFCF6C5132B354F28D3B0B7D38367",
+ "019F4113742A2B14BD25926B49C649155F267E60D3814B4C0CC84250E46F0083",
+ SHA256
+ ),
+ (
+ "test",
+ "16AEFFA357260B04B1DD199693960740066C1A8F3E8EDD79070AA914D361B3B8",
+ "83910E8B48BB0C74244EBDF7F07A1C5413D61472BD941EF3920E623FBCCEBEB6",
+ "8DDBEC54CF8CD5874883841D712142A56A8D0F218F5003CB0296B6B509619F2C",
+ SHA384
+ ),
+ (
+ "test",
+ "6915D11632ACA3C40D5D51C08DAF9C555933819548784480E93499000D9F0B7F",
+ "461D93F31B6540894788FD206C07CFA0CC35F46FA3C91816FFF1040AD1581A04",
+ "39AF9F15DE0DB8D97E72719C74820D304CE5226E32DEDAE67519E840D1194E55",
+ SHA512
+ )
+ )
+
+ signatures_p384_ = (
+ (
+ "sample",
+ "4471EF7518BB2C7C20F62EAE1C387AD0C5E8E470995DB4ACF694466E6AB096630F29E5938D25106C3C340045A2DB01A7",
+ "EC748D839243D6FBEF4FC5C4859A7DFFD7F3ABDDF72014540C16D73309834FA37B9BA002899F6FDA3A4A9386790D4EB2",
+ "A3BCFA947BEEF4732BF247AC17F71676CB31A847B9FF0CBC9C9ED4C1A5B3FACF26F49CA031D4857570CCB5CA4424A443",
+ SHA1
+ ),
+ (
+ "sample",
+ "A4E4D2F0E729EB786B31FC20AD5D849E304450E0AE8E3E341134A5C1AFA03CAB8083EE4E3C45B06A5899EA56C51B5879",
+ "42356E76B55A6D9B4631C865445DBE54E056D3B3431766D0509244793C3F9366450F76EE3DE43F5A125333A6BE060122",
+ "9DA0C81787064021E78DF658F2FBB0B042BF304665DB721F077A4298B095E4834C082C03D83028EFBF93A3C23940CA8D",
+ SHA224
+ ),
+ (
+ "sample",
+ "180AE9F9AEC5438A44BC159A1FCB277C7BE54FA20E7CF404B490650A8ACC414E375572342863C899F9F2EDF9747A9B60",
+ "21B13D1E013C7FA1392D03C5F99AF8B30C570C6F98D4EA8E354B63A21D3DAA33BDE1E888E63355D92FA2B3C36D8FB2CD",
+ "F3AA443FB107745BF4BD77CB3891674632068A10CA67E3D45DB2266FA7D1FEEBEFDC63ECCD1AC42EC0CB8668A4FA0AB0",
+ SHA256
+ ),
+ (
+ "sample",
+ "94ED910D1A099DAD3254E9242AE85ABDE4BA15168EAF0CA87A555FD56D10FBCA2907E3E83BA95368623B8C4686915CF9",
+ "94EDBB92A5ECB8AAD4736E56C691916B3F88140666CE9FA73D64C4EA95AD133C81A648152E44ACF96E36DD1E80FABE46",
+ "99EF4AEB15F178CEA1FE40DB2603138F130E740A19624526203B6351D0A3A94FA329C145786E679E7B82C71A38628AC8",
+ SHA384
+ ),
+ (
+ "sample",
+ "92FC3C7183A883E24216D1141F1A8976C5B0DD797DFA597E3D7B32198BD35331A4E966532593A52980D0E3AAA5E10EC3",
+ "ED0959D5880AB2D869AE7F6C2915C6D60F96507F9CB3E047C0046861DA4A799CFE30F35CC900056D7C99CD7882433709",
+ "512C8CCEEE3890A84058CE1E22DBC2198F42323CE8ACA9135329F03C068E5112DC7CC3EF3446DEFCEB01A45C2667FDD5",
+ SHA512
+ ),
+ (
+ "test",
+ "66CC2C8F4D303FC962E5FF6A27BD79F84EC812DDAE58CF5243B64A4AD8094D47EC3727F3A3C186C15054492E30698497",
+ "4BC35D3A50EF4E30576F58CD96CE6BF638025EE624004A1F7789A8B8E43D0678ACD9D29876DAF46638645F7F404B11C7",
+ "D5A6326C494ED3FF614703878961C0FDE7B2C278F9A65FD8C4B7186201A2991695BA1C84541327E966FA7B50F7382282",
+ SHA1
+ ),
+ (
+ "test",
+ "18FA39DB95AA5F561F30FA3591DC59C0FA3653A80DAFFA0B48D1A4C6DFCBFF6E3D33BE4DC5EB8886A8ECD093F2935726",
+ "E8C9D0B6EA72A0E7837FEA1D14A1A9557F29FAA45D3E7EE888FC5BF954B5E62464A9A817C47FF78B8C11066B24080E72",
+ "07041D4A7A0379AC7232FF72E6F77B6DDB8F09B16CCE0EC3286B2BD43FA8C6141C53EA5ABEF0D8231077A04540A96B66",
+ SHA224
+ ),
+ (
+ "test",
+ "0CFAC37587532347DC3389FDC98286BBA8C73807285B184C83E62E26C401C0FAA48DD070BA79921A3457ABFF2D630AD7",
+ "6D6DEFAC9AB64DABAFE36C6BF510352A4CC27001263638E5B16D9BB51D451559F918EEDAF2293BE5B475CC8F0188636B",
+ "2D46F3BECBCC523D5F1A1256BF0C9B024D879BA9E838144C8BA6BAEB4B53B47D51AB373F9845C0514EEFB14024787265",
+ SHA256
+ ),
+ (
+ "test",
+ "015EE46A5BF88773ED9123A5AB0807962D193719503C527B031B4C2D225092ADA71F4A459BC0DA98ADB95837DB8312EA",
+ "8203B63D3C853E8D77227FB377BCF7B7B772E97892A80F36AB775D509D7A5FEB0542A7F0812998DA8F1DD3CA3CF023DB",
+ "DDD0760448D42D8A43AF45AF836FCE4DE8BE06B485E9B61B827C2F13173923E06A739F040649A667BF3B828246BAA5A5",
+ SHA384
+ ),
+ (
+ "test",
+ "3780C4F67CB15518B6ACAE34C9F83568D2E12E47DEAB6C50A4E4EE5319D1E8CE0E2CC8A136036DC4B9C00E6888F66B6C",
+ "A0D5D090C9980FAF3C2CE57B7AE951D31977DD11C775D314AF55F76C676447D06FB6495CD21B4B6E340FC236584FB277",
+ "976984E59B4C77B0E8E4460DCA3D9F20E07B9BB1F63BEEFAF576F6B2E8B224634A2092CD3792E0159AD9CEE37659C736",
+ SHA512
+ ),
+ )
+
+ signatures_p521_ = (
+ (
+ "sample",
+ "0089C071B419E1C2820962321787258469511958E80582E95D8378E0C2CCDB3CB42BEDE42F50E3FA3C71F5A76724281D31D9C89F0F91FC1BE4918DB1C03A5838D0F9",
+ "00343B6EC45728975EA5CBA6659BBB6062A5FF89EEA58BE3C80B619F322C87910FE092F7D45BB0F8EEE01ED3F20BABEC079D202AE677B243AB40B5431D497C55D75D",
+ "00E7B0E675A9B24413D448B8CC119D2BF7B2D2DF032741C096634D6D65D0DBE3D5694625FB9E8104D3B842C1B0E2D0B98BEA19341E8676AEF66AE4EBA3D5475D5D16",
+ SHA1
+ ),
+ (
+ "sample",
+ "0121415EC2CD7726330A61F7F3FA5DE14BE9436019C4DB8CB4041F3B54CF31BE0493EE3F427FB906393D895A19C9523F3A1D54BB8702BD4AA9C99DAB2597B92113F3",
+ "01776331CFCDF927D666E032E00CF776187BC9FDD8E69D0DABB4109FFE1B5E2A30715F4CC923A4A5E94D2503E9ACFED92857B7F31D7152E0F8C00C15FF3D87E2ED2E",
+ "0050CB5265417FE2320BBB5A122B8E1A32BD699089851128E360E620A30C7E17BA41A666AF126CE100E5799B153B60528D5300D08489CA9178FB610A2006C254B41F",
+ SHA224
+ ),
+ (
+ "sample",
+ "00EDF38AFCAAECAB4383358B34D67C9F2216C8382AAEA44A3DAD5FDC9C32575761793FEF24EB0FC276DFC4F6E3EC476752F043CF01415387470BCBD8678ED2C7E1A0",
+ "01511BB4D675114FE266FC4372B87682BAECC01D3CC62CF2303C92B3526012659D16876E25C7C1E57648F23B73564D67F61C6F14D527D54972810421E7D87589E1A7",
+ "004A171143A83163D6DF460AAF61522695F207A58B95C0644D87E52AA1A347916E4F7A72930B1BC06DBE22CE3F58264AFD23704CBB63B29B931F7DE6C9D949A7ECFC",
+ SHA256
+ ),
+ (
+ "sample",
+ "01546A108BC23A15D6F21872F7DED661FA8431DDBD922D0DCDB77CC878C8553FFAD064C95A920A750AC9137E527390D2D92F153E66196966EA554D9ADFCB109C4211",
+ "01EA842A0E17D2DE4F92C15315C63DDF72685C18195C2BB95E572B9C5136CA4B4B576AD712A52BE9730627D16054BA40CC0B8D3FF035B12AE75168397F5D50C67451",
+ "01F21A3CEE066E1961025FB048BD5FE2B7924D0CD797BABE0A83B66F1E35EEAF5FDE143FA85DC394A7DEE766523393784484BDF3E00114A1C857CDE1AA203DB65D61",
+ SHA384
+ ),
+ (
+ "sample",
+ "01DAE2EA071F8110DC26882D4D5EAE0621A3256FC8847FB9022E2B7D28E6F10198B1574FDD03A9053C08A1854A168AA5A57470EC97DD5CE090124EF52A2F7ECBFFD3",
+ "00C328FAFCBD79DD77850370C46325D987CB525569FB63C5D3BC53950E6D4C5F174E25A1EE9017B5D450606ADD152B534931D7D4E8455CC91F9B15BF05EC36E377FA",
+ "00617CCE7CF5064806C467F678D3B4080D6F1CC50AF26CA209417308281B68AF282623EAA63E5B5C0723D8B8C37FF0777B1A20F8CCB1DCCC43997F1EE0E44DA4A67A",
+ SHA512
+ ),
+ (
+ "test",
+ "00BB9F2BF4FE1038CCF4DABD7139A56F6FD8BB1386561BD3C6A4FC818B20DF5DDBA80795A947107A1AB9D12DAA615B1ADE4F7A9DC05E8E6311150F47F5C57CE8B222",
+ "013BAD9F29ABE20DE37EBEB823C252CA0F63361284015A3BF430A46AAA80B87B0693F0694BD88AFE4E661FC33B094CD3B7963BED5A727ED8BD6A3A202ABE009D0367",
+ "01E9BB81FF7944CA409AD138DBBEE228E1AFCC0C890FC78EC8604639CB0DBDC90F717A99EAD9D272855D00162EE9527567DD6A92CBD629805C0445282BBC916797FF",
+ SHA1
+ ),
+ (
+ "test",
+ "0040D09FCF3C8A5F62CF4FB223CBBB2B9937F6B0577C27020A99602C25A01136987E452988781484EDBBCF1C47E554E7FC901BC3085E5206D9F619CFF07E73D6F706",
+ "01C7ED902E123E6815546065A2C4AF977B22AA8EADDB68B2C1110E7EA44D42086BFE4A34B67DDC0E17E96536E358219B23A706C6A6E16BA77B65E1C595D43CAE17FB",
+ "0177336676304FCB343CE028B38E7B4FBA76C1C1B277DA18CAD2A8478B2A9A9F5BEC0F3BA04F35DB3E4263569EC6AADE8C92746E4C82F8299AE1B8F1739F8FD519A4",
+ SHA224
+ ),
+ (
+ "test",
+ "001DE74955EFAABC4C4F17F8E84D881D1310B5392D7700275F82F145C61E843841AF09035BF7A6210F5A431A6A9E81C9323354A9E69135D44EBD2FCAA7731B909258",
+ "000E871C4A14F993C6C7369501900C4BC1E9C7B0B4BA44E04868B30B41D8071042EB28C4C250411D0CE08CD197E4188EA4876F279F90B3D8D74A3C76E6F1E4656AA8",
+ "00CD52DBAA33B063C3A6CD8058A1FB0A46A4754B034FCC644766CA14DA8CA5CA9FDE00E88C1AD60CCBA759025299079D7A427EC3CC5B619BFBC828E7769BCD694E86",
+ SHA256
+ ),
+ (
+ "test",
+ "01F1FC4A349A7DA9A9E116BFDD055DC08E78252FF8E23AC276AC88B1770AE0B5DCEB1ED14A4916B769A523CE1E90BA22846AF11DF8B300C38818F713DADD85DE0C88",
+ "014BEE21A18B6D8B3C93FAB08D43E739707953244FDBE924FA926D76669E7AC8C89DF62ED8975C2D8397A65A49DCC09F6B0AC62272741924D479354D74FF6075578C",
+ "0133330865C067A0EAF72362A65E2D7BC4E461E8C8995C3B6226A21BD1AA78F0ED94FE536A0DCA35534F0CD1510C41525D163FE9D74D134881E35141ED5E8E95B979",
+ SHA384
+ ),
+ (
+ "test",
+ "016200813020EC986863BEDFC1B121F605C1215645018AEA1A7B215A564DE9EB1B38A67AA1128B80CE391C4FB71187654AAA3431027BFC7F395766CA988C964DC56D",
+ "013E99020ABF5CEE7525D16B69B229652AB6BDF2AFFCAEF38773B4B7D08725F10CDB93482FDCC54EDCEE91ECA4166B2A7C6265EF0CE2BD7051B7CEF945BABD47EE6D",
+ "01FBD0013C674AA79CB39849527916CE301C66EA7CE8B80682786AD60F98F7E78A19CA69EFF5C57400E3B3A0AD66CE0978214D13BAF4E9AC60752F7B155E2DE4DCE3",
+ SHA512
+ ),
+ )
+
+ signatures_p192 = []
+ for a, b, c, d, e in signatures_p192_:
+ new_tv = (tobytes(a), unhexlify(b), unhexlify(c), unhexlify(d), e)
+ signatures_p192.append(new_tv)
+
+ signatures_p224 = []
+ for a, b, c, d, e in signatures_p224_:
+ new_tv = (tobytes(a), unhexlify(b), unhexlify(c), unhexlify(d), e)
+ signatures_p224.append(new_tv)
+
+ signatures_p256 = []
+ for a, b, c, d, e in signatures_p256_:
+ new_tv = (tobytes(a), unhexlify(b), unhexlify(c), unhexlify(d), e)
+ signatures_p256.append(new_tv)
+
+ signatures_p384 = []
+ for a, b, c, d, e in signatures_p384_:
+ new_tv = (tobytes(a), unhexlify(b), unhexlify(c), unhexlify(d), e)
+ signatures_p384.append(new_tv)
+
+ signatures_p521 = []
+ for a, b, c, d, e in signatures_p521_:
+ new_tv = (tobytes(a), unhexlify(b), unhexlify(c), unhexlify(d), e)
+ signatures_p521.append(new_tv)
+
+ def shortDescription(self):
+ return "Deterministic ECDSA Tests"
+
+ def test_loopback_p192(self):
+ hashed_msg = SHA512.new(b"test")
+ signer = DSS.new(self.key_priv_p192, 'deterministic-rfc6979')
+ signature = signer.sign(hashed_msg)
+
+ verifier = DSS.new(self.key_pub_p192, 'deterministic-rfc6979')
+ verifier.verify(hashed_msg, signature)
+
+ def test_loopback_p224(self):
+ hashed_msg = SHA512.new(b"test")
+ signer = DSS.new(self.key_priv_p224, 'deterministic-rfc6979')
+ signature = signer.sign(hashed_msg)
+
+ verifier = DSS.new(self.key_pub_p224, 'deterministic-rfc6979')
+ verifier.verify(hashed_msg, signature)
+
+ def test_loopback_p256(self):
+ hashed_msg = SHA512.new(b"test")
+ signer = DSS.new(self.key_priv_p256, 'deterministic-rfc6979')
+ signature = signer.sign(hashed_msg)
+
+ verifier = DSS.new(self.key_pub_p256, 'deterministic-rfc6979')
+ verifier.verify(hashed_msg, signature)
+
+ def test_loopback_p384(self):
+ hashed_msg = SHA512.new(b"test")
+ signer = DSS.new(self.key_priv_p384, 'deterministic-rfc6979')
+ signature = signer.sign(hashed_msg)
+
+ verifier = DSS.new(self.key_pub_p384, 'deterministic-rfc6979')
+ verifier.verify(hashed_msg, signature)
+
+ def test_loopback_p521(self):
+ hashed_msg = SHA512.new(b"test")
+ signer = DSS.new(self.key_priv_p521, 'deterministic-rfc6979')
+ signature = signer.sign(hashed_msg)
+
+ verifier = DSS.new(self.key_pub_p521, 'deterministic-rfc6979')
+ verifier.verify(hashed_msg, signature)
+
+ def test_data_rfc6979_p192(self):
+ signer = DSS.new(self.key_priv_p192, 'deterministic-rfc6979')
+ for message, k, r, s, module in self.signatures_p192:
+ hash_obj = module.new(message)
+ result = signer.sign(hash_obj)
+ self.assertEqual(r + s, result)
+
+ def test_data_rfc6979_p224(self):
+ signer = DSS.new(self.key_priv_p224, 'deterministic-rfc6979')
+ for message, k, r, s, module in self.signatures_p224:
+ hash_obj = module.new(message)
+ result = signer.sign(hash_obj)
+ self.assertEqual(r + s, result)
+
+ def test_data_rfc6979_p256(self):
+ signer = DSS.new(self.key_priv_p256, 'deterministic-rfc6979')
+ for message, k, r, s, module in self.signatures_p256:
+ hash_obj = module.new(message)
+ result = signer.sign(hash_obj)
+ self.assertEqual(r + s, result)
+
+ def test_data_rfc6979_p384(self):
+ signer = DSS.new(self.key_priv_p384, 'deterministic-rfc6979')
+ for message, k, r, s, module in self.signatures_p384:
+ hash_obj = module.new(message)
+ result = signer.sign(hash_obj)
+ self.assertEqual(r + s, result)
+
+ def test_data_rfc6979_p521(self):
+ signer = DSS.new(self.key_priv_p521, 'deterministic-rfc6979')
+ for message, k, r, s, module in self.signatures_p521:
+ hash_obj = module.new(message)
+ result = signer.sign(hash_obj)
+ self.assertEqual(r + s, result)
+
+
+def get_hash_module(hash_name):
+ if hash_name == "SHA-512":
+ hash_module = SHA512
+ elif hash_name == "SHA-512/224":
+ hash_module = SHA512.new(truncate="224")
+ elif hash_name == "SHA-512/256":
+ hash_module = SHA512.new(truncate="256")
+ elif hash_name == "SHA-384":
+ hash_module = SHA384
+ elif hash_name == "SHA-256":
+ hash_module = SHA256
+ elif hash_name == "SHA-224":
+ hash_module = SHA224
+ elif hash_name == "SHA-1":
+ hash_module = SHA1
+ elif hash_name == "SHA3-224":
+ hash_module = SHA3_224
+ elif hash_name == "SHA3-256":
+ hash_module = SHA3_256
+ elif hash_name == "SHA3-384":
+ hash_module = SHA3_384
+ elif hash_name == "SHA3-512":
+ hash_module = SHA3_512
+ else:
+ raise ValueError("Unknown hash algorithm: " + hash_name)
+ return hash_module
+
+
+class TestVectorsDSAWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings, slow_tests):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._slow_tests = slow_tests
+ self._id = "None"
+ self.tv = []
+
+ def setUp(self):
+
+ def filter_dsa(group):
+ return DSA.import_key(group['keyPem'])
+
+ def filter_sha(group):
+ return get_hash_module(group['sha'])
+
+ def filter_type(group):
+ sig_type = group['type']
+ if sig_type != 'DsaVerify':
+ raise ValueError("Unknown signature type " + sig_type)
+ return sig_type
+
+ result = load_test_vectors_wycheproof(("Signature", "wycheproof"),
+ "dsa_test.json",
+ "Wycheproof DSA signature",
+ group_tag={'key': filter_dsa,
+ 'hash_module': filter_sha,
+ 'sig_type': filter_type})
+ self.tv += result
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_verify(self, tv):
+ self._id = "Wycheproof DSA Test #" + str(tv.id)
+
+ hashed_msg = tv.hash_module.new(tv.msg)
+ signer = DSS.new(tv.key, 'fips-186-3', encoding='der')
+ try:
+ signature = signer.verify(hashed_msg, tv.sig)
+ except ValueError as e:
+ if tv.warning:
+ return
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.warn(tv)
+
+ def runTest(self):
+ for tv in self.tv:
+ self.test_verify(tv)
+
+
+class TestVectorsECDSAWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings, slow_tests):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._slow_tests = slow_tests
+ self._id = "None"
+
+ def add_tests(self, filename):
+
+ def filter_ecc(group):
+ # These are the only curves we accept to skip
+ if group['key']['curve'] in ('secp224k1', 'secp256k1',
+ 'brainpoolP224r1', 'brainpoolP224t1',
+ 'brainpoolP256r1', 'brainpoolP256t1',
+ 'brainpoolP320r1', 'brainpoolP320t1',
+ 'brainpoolP384r1', 'brainpoolP384t1',
+ 'brainpoolP512r1', 'brainpoolP512t1',
+ ):
+ return None
+ return ECC.import_key(group['keyPem'])
+
+ def filter_sha(group):
+ return get_hash_module(group['sha'])
+
+ def filter_encoding(group):
+ encoding_name = group['type']
+ if encoding_name == "EcdsaVerify":
+ return "der"
+ elif encoding_name == "EcdsaP1363Verify":
+ return "binary"
+ else:
+ raise ValueError("Unknown signature type " + encoding_name)
+
+ result = load_test_vectors_wycheproof(("Signature", "wycheproof"),
+ filename,
+ "Wycheproof ECDSA signature (%s)" % filename,
+ group_tag={'key': filter_ecc,
+ 'hash_module': filter_sha,
+ 'encoding': filter_encoding,
+ })
+ self.tv += result
+
+ def setUp(self):
+ self.tv = []
+ self.add_tests("ecdsa_secp224r1_sha224_p1363_test.json")
+ self.add_tests("ecdsa_secp224r1_sha224_test.json")
+ if self._slow_tests:
+ self.add_tests("ecdsa_secp224r1_sha256_p1363_test.json")
+ self.add_tests("ecdsa_secp224r1_sha256_test.json")
+ self.add_tests("ecdsa_secp224r1_sha3_224_test.json")
+ self.add_tests("ecdsa_secp224r1_sha3_256_test.json")
+ self.add_tests("ecdsa_secp224r1_sha3_512_test.json")
+ self.add_tests("ecdsa_secp224r1_sha512_p1363_test.json")
+ self.add_tests("ecdsa_secp224r1_sha512_test.json")
+ self.add_tests("ecdsa_secp256r1_sha256_p1363_test.json")
+ self.add_tests("ecdsa_secp256r1_sha256_test.json")
+ self.add_tests("ecdsa_secp256r1_sha3_256_test.json")
+ self.add_tests("ecdsa_secp256r1_sha3_512_test.json")
+ self.add_tests("ecdsa_secp256r1_sha512_p1363_test.json")
+ self.add_tests("ecdsa_secp256r1_sha512_test.json")
+ if self._slow_tests:
+ self.add_tests("ecdsa_secp384r1_sha3_384_test.json")
+ self.add_tests("ecdsa_secp384r1_sha3_512_test.json")
+ self.add_tests("ecdsa_secp384r1_sha384_p1363_test.json")
+ self.add_tests("ecdsa_secp384r1_sha384_test.json")
+ self.add_tests("ecdsa_secp384r1_sha512_p1363_test.json")
+ self.add_tests("ecdsa_secp384r1_sha512_test.json")
+ if self._slow_tests:
+ self.add_tests("ecdsa_secp521r1_sha3_512_test.json")
+ self.add_tests("ecdsa_secp521r1_sha512_p1363_test.json")
+ self.add_tests("ecdsa_secp521r1_sha512_test.json")
+ self.add_tests("ecdsa_test.json")
+ self.add_tests("ecdsa_webcrypto_test.json")
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_verify(self, tv):
+ self._id = "Wycheproof ECDSA Test #%d (%s, %s)" % (tv.id, tv.comment, tv.filename)
+
+ # Skip tests with unsupported curves
+ if tv.key is None:
+ return
+
+ hashed_msg = tv.hash_module.new(tv.msg)
+ signer = DSS.new(tv.key, 'fips-186-3', encoding=tv.encoding)
+ try:
+ signature = signer.verify(hashed_msg, tv.sig)
+ except ValueError as e:
+ if tv.warning:
+ return
+ if tv.comment == "k*G has a large x-coordinate":
+ return
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.warn(tv)
+
+ def runTest(self):
+ for tv in self.tv:
+ self.test_verify(tv)
+
+
+def get_tests(config={}):
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ tests = []
+ tests += list_test_cases(FIPS_DSA_Tests)
+ tests += list_test_cases(FIPS_ECDSA_Tests)
+ tests += list_test_cases(Det_DSA_Tests)
+ tests += list_test_cases(Det_ECDSA_Tests)
+
+ slow_tests = config.get('slow_tests')
+ if slow_tests:
+ tests += list_test_cases(FIPS_DSA_Tests_KAT)
+ tests += list_test_cases(FIPS_ECDSA_Tests_KAT)
+
+ tests += [TestVectorsDSAWycheproof(wycheproof_warnings, slow_tests)]
+ tests += [TestVectorsECDSAWycheproof(wycheproof_warnings, slow_tests)]
+
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_eddsa.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_eddsa.py
new file mode 100644
index 00000000..cca0994d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_eddsa.py
@@ -0,0 +1,604 @@
+#
+# Copyright (c) 2022, Legrandin
+# 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 unittest
+from binascii import unhexlify
+
+from Crypto.PublicKey import ECC
+from Crypto.Signature import eddsa
+from Crypto.Hash import SHA512, SHAKE256
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors_wycheproof
+from Crypto.Util.number import bytes_to_long
+
+rfc8032_tv_str = (
+ # 7.1 Ed25519
+ (
+ "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
+ "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
+ "",
+ None,
+ "",
+ "e5564300c360ac729086e2cc806e828a"
+ "84877f1eb8e5d974d873e06522490155"
+ "5fb8821590a33bacc61e39701cf9b46b"
+ "d25bf5f0595bbe24655141438e7a100b"
+ ),
+ (
+ "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
+ "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c",
+ "72",
+ None,
+ "",
+ "92a009a9f0d4cab8720e820b5f642540"
+ "a2b27b5416503f8fb3762223ebdb69da"
+ "085ac1e43e15996e458f3613d0f11d8c"
+ "387b2eaeb4302aeeb00d291612bb0c00"
+ ),
+ (
+ "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
+ "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025",
+ "af82",
+ None,
+ "",
+ "6291d657deec24024827e69c3abe01a3"
+ "0ce548a284743a445e3680d7db5ac3ac"
+ "18ff9b538d16f290ae67f760984dc659"
+ "4a7c15e9716ed28dc027beceea1ec40a"
+ ),
+ (
+ "f5e5767cf153319517630f226876b86c8160cc583bc013744c6bf255f5cc0ee5",
+ "278117fc144c72340f67d0f2316e8386ceffbf2b2428c9c51fef7c597f1d426e",
+ "08b8b2b733424243760fe426a4b54908"
+ "632110a66c2f6591eabd3345e3e4eb98"
+ "fa6e264bf09efe12ee50f8f54e9f77b1"
+ "e355f6c50544e23fb1433ddf73be84d8"
+ "79de7c0046dc4996d9e773f4bc9efe57"
+ "38829adb26c81b37c93a1b270b20329d"
+ "658675fc6ea534e0810a4432826bf58c"
+ "941efb65d57a338bbd2e26640f89ffbc"
+ "1a858efcb8550ee3a5e1998bd177e93a"
+ "7363c344fe6b199ee5d02e82d522c4fe"
+ "ba15452f80288a821a579116ec6dad2b"
+ "3b310da903401aa62100ab5d1a36553e"
+ "06203b33890cc9b832f79ef80560ccb9"
+ "a39ce767967ed628c6ad573cb116dbef"
+ "efd75499da96bd68a8a97b928a8bbc10"
+ "3b6621fcde2beca1231d206be6cd9ec7"
+ "aff6f6c94fcd7204ed3455c68c83f4a4"
+ "1da4af2b74ef5c53f1d8ac70bdcb7ed1"
+ "85ce81bd84359d44254d95629e9855a9"
+ "4a7c1958d1f8ada5d0532ed8a5aa3fb2"
+ "d17ba70eb6248e594e1a2297acbbb39d"
+ "502f1a8c6eb6f1ce22b3de1a1f40cc24"
+ "554119a831a9aad6079cad88425de6bd"
+ "e1a9187ebb6092cf67bf2b13fd65f270"
+ "88d78b7e883c8759d2c4f5c65adb7553"
+ "878ad575f9fad878e80a0c9ba63bcbcc"
+ "2732e69485bbc9c90bfbd62481d9089b"
+ "eccf80cfe2df16a2cf65bd92dd597b07"
+ "07e0917af48bbb75fed413d238f5555a"
+ "7a569d80c3414a8d0859dc65a46128ba"
+ "b27af87a71314f318c782b23ebfe808b"
+ "82b0ce26401d2e22f04d83d1255dc51a"
+ "ddd3b75a2b1ae0784504df543af8969b"
+ "e3ea7082ff7fc9888c144da2af58429e"
+ "c96031dbcad3dad9af0dcbaaaf268cb8"
+ "fcffead94f3c7ca495e056a9b47acdb7"
+ "51fb73e666c6c655ade8297297d07ad1"
+ "ba5e43f1bca32301651339e22904cc8c"
+ "42f58c30c04aafdb038dda0847dd988d"
+ "cda6f3bfd15c4b4c4525004aa06eeff8"
+ "ca61783aacec57fb3d1f92b0fe2fd1a8"
+ "5f6724517b65e614ad6808d6f6ee34df"
+ "f7310fdc82aebfd904b01e1dc54b2927"
+ "094b2db68d6f903b68401adebf5a7e08"
+ "d78ff4ef5d63653a65040cf9bfd4aca7"
+ "984a74d37145986780fc0b16ac451649"
+ "de6188a7dbdf191f64b5fc5e2ab47b57"
+ "f7f7276cd419c17a3ca8e1b939ae49e4"
+ "88acba6b965610b5480109c8b17b80e1"
+ "b7b750dfc7598d5d5011fd2dcc5600a3"
+ "2ef5b52a1ecc820e308aa342721aac09"
+ "43bf6686b64b2579376504ccc493d97e"
+ "6aed3fb0f9cd71a43dd497f01f17c0e2"
+ "cb3797aa2a2f256656168e6c496afc5f"
+ "b93246f6b1116398a346f1a641f3b041"
+ "e989f7914f90cc2c7fff357876e506b5"
+ "0d334ba77c225bc307ba537152f3f161"
+ "0e4eafe595f6d9d90d11faa933a15ef1"
+ "369546868a7f3a45a96768d40fd9d034"
+ "12c091c6315cf4fde7cb68606937380d"
+ "b2eaaa707b4c4185c32eddcdd306705e"
+ "4dc1ffc872eeee475a64dfac86aba41c"
+ "0618983f8741c5ef68d3a101e8a3b8ca"
+ "c60c905c15fc910840b94c00a0b9d0",
+ None,
+ "",
+ "0aab4c900501b3e24d7cdf4663326a3a"
+ "87df5e4843b2cbdb67cbf6e460fec350"
+ "aa5371b1508f9f4528ecea23c436d94b"
+ "5e8fcd4f681e30a6ac00a9704a188a03"
+ ),
+ # 7.2 Ed25519ctx
+ (
+ "0305334e381af78f141cb666f6199f57"
+ "bc3495335a256a95bd2a55bf546663f6",
+ "dfc9425e4f968f7f0c29f0259cf5f9ae"
+ "d6851c2bb4ad8bfb860cfee0ab248292",
+ "f726936d19c800494e3fdaff20b276a8",
+ None,
+ "666f6f",
+ "55a4cc2f70a54e04288c5f4cd1e45a7b"
+ "b520b36292911876cada7323198dd87a"
+ "8b36950b95130022907a7fb7c4e9b2d5"
+ "f6cca685a587b4b21f4b888e4e7edb0d"
+ ),
+ (
+ "0305334e381af78f141cb666f6199f57"
+ "bc3495335a256a95bd2a55bf546663f6",
+ "dfc9425e4f968f7f0c29f0259cf5f9ae"
+ "d6851c2bb4ad8bfb860cfee0ab248292",
+ "f726936d19c800494e3fdaff20b276a8",
+ None,
+ "626172",
+ "fc60d5872fc46b3aa69f8b5b4351d580"
+ "8f92bcc044606db097abab6dbcb1aee3"
+ "216c48e8b3b66431b5b186d1d28f8ee1"
+ "5a5ca2df6668346291c2043d4eb3e90d"
+ ),
+ (
+ "0305334e381af78f141cb666f6199f57"
+ "bc3495335a256a95bd2a55bf546663f6",
+ "dfc9425e4f968f7f0c29f0259cf5f9ae"
+ "d6851c2bb4ad8bfb860cfee0ab248292",
+ "508e9e6882b979fea900f62adceaca35",
+ None,
+ "666f6f",
+ "8b70c1cc8310e1de20ac53ce28ae6e72"
+ "07f33c3295e03bb5c0732a1d20dc6490"
+ "8922a8b052cf99b7c4fe107a5abb5b2c"
+ "4085ae75890d02df26269d8945f84b0b"
+ ),
+ (
+ "ab9c2853ce297ddab85c993b3ae14bca"
+ "d39b2c682beabc27d6d4eb20711d6560",
+ "0f1d1274943b91415889152e893d80e9"
+ "3275a1fc0b65fd71b4b0dda10ad7d772",
+ "f726936d19c800494e3fdaff20b276a8",
+ None,
+ "666f6f",
+ "21655b5f1aa965996b3f97b3c849eafb"
+ "a922a0a62992f73b3d1b73106a84ad85"
+ "e9b86a7b6005ea868337ff2d20a7f5fb"
+ "d4cd10b0be49a68da2b2e0dc0ad8960f"
+ ),
+ # 7.3 Ed25519ph
+ (
+ "833fe62409237b9d62ec77587520911e"
+ "9a759cec1d19755b7da901b96dca3d42",
+ "ec172b93ad5e563bf4932c70e1245034"
+ "c35467ef2efd4d64ebf819683467e2bf",
+ "616263",
+ SHA512,
+ "",
+ "98a70222f0b8121aa9d30f813d683f80"
+ "9e462b469c7ff87639499bb94e6dae41"
+ "31f85042463c2a355a2003d062adf5aa"
+ "a10b8c61e636062aaad11c2a26083406"
+ ),
+ # 7.4 Ed448
+ (
+ "6c82a562cb808d10d632be89c8513ebf6c929f34ddfa8c9f63c9960ef6e348a3"
+ "528c8a3fcc2f044e39a3fc5b94492f8f032e7549a20098f95b",
+ "5fd7449b59b461fd2ce787ec616ad46a1da1342485a70e1f8a0ea75d80e96778"
+ "edf124769b46c7061bd6783df1e50f6cd1fa1abeafe8256180",
+ "",
+ None,
+ "",
+ "533a37f6bbe457251f023c0d88f976ae2dfb504a843e34d2074fd823d41a591f"
+ "2b233f034f628281f2fd7a22ddd47d7828c59bd0a21bfd3980ff0d2028d4b18a"
+ "9df63e006c5d1c2d345b925d8dc00b4104852db99ac5c7cdda8530a113a0f4db"
+ "b61149f05a7363268c71d95808ff2e652600"
+ ),
+ (
+ "c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463a"
+ "fbea67c5e8d2877c5e3bc397a659949ef8021e954e0a12274e",
+ "43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c086"
+ "6aea01eb00742802b8438ea4cb82169c235160627b4c3a9480",
+ "03",
+ None,
+ "",
+ "26b8f91727bd62897af15e41eb43c377efb9c610d48f2335cb0bd0087810f435"
+ "2541b143c4b981b7e18f62de8ccdf633fc1bf037ab7cd779805e0dbcc0aae1cb"
+ "cee1afb2e027df36bc04dcecbf154336c19f0af7e0a6472905e799f1953d2a0f"
+ "f3348ab21aa4adafd1d234441cf807c03a00",
+ ),
+ (
+ "c4eab05d357007c632f3dbb48489924d552b08fe0c353a0d4a1f00acda2c463a"
+ "fbea67c5e8d2877c5e3bc397a659949ef8021e954e0a12274e",
+ "43ba28f430cdff456ae531545f7ecd0ac834a55d9358c0372bfa0c6c6798c086"
+ "6aea01eb00742802b8438ea4cb82169c235160627b4c3a9480",
+ "03",
+ None,
+ "666f6f",
+ "d4f8f6131770dd46f40867d6fd5d5055de43541f8c5e35abbcd001b32a89f7d2"
+ "151f7647f11d8ca2ae279fb842d607217fce6e042f6815ea000c85741de5c8da"
+ "1144a6a1aba7f96de42505d7a7298524fda538fccbbb754f578c1cad10d54d0d"
+ "5428407e85dcbc98a49155c13764e66c3c00",
+ ),
+ (
+ "cd23d24f714274e744343237b93290f511f6425f98e64459ff203e8985083ffd"
+ "f60500553abc0e05cd02184bdb89c4ccd67e187951267eb328",
+ "dcea9e78f35a1bf3499a831b10b86c90aac01cd84b67a0109b55a36e9328b1e3"
+ "65fce161d71ce7131a543ea4cb5f7e9f1d8b00696447001400",
+ "0c3e544074ec63b0265e0c",
+ None,
+ "",
+ "1f0a8888ce25e8d458a21130879b840a9089d999aaba039eaf3e3afa090a09d3"
+ "89dba82c4ff2ae8ac5cdfb7c55e94d5d961a29fe0109941e00b8dbdeea6d3b05"
+ "1068df7254c0cdc129cbe62db2dc957dbb47b51fd3f213fb8698f064774250a5"
+ "028961c9bf8ffd973fe5d5c206492b140e00",
+ ),
+ (
+ "258cdd4ada32ed9c9ff54e63756ae582fb8fab2ac721f2c8e676a72768513d93"
+ "9f63dddb55609133f29adf86ec9929dccb52c1c5fd2ff7e21b",
+ "3ba16da0c6f2cc1f30187740756f5e798d6bc5fc015d7c63cc9510ee3fd44adc"
+ "24d8e968b6e46e6f94d19b945361726bd75e149ef09817f580",
+ "64a65f3cdedcdd66811e2915",
+ None,
+ "",
+ "7eeeab7c4e50fb799b418ee5e3197ff6bf15d43a14c34389b59dd1a7b1b85b4a"
+ "e90438aca634bea45e3a2695f1270f07fdcdf7c62b8efeaf00b45c2c96ba457e"
+ "b1a8bf075a3db28e5c24f6b923ed4ad747c3c9e03c7079efb87cb110d3a99861"
+ "e72003cbae6d6b8b827e4e6c143064ff3c00",
+ ),
+ (
+ "7ef4e84544236752fbb56b8f31a23a10e42814f5f55ca037cdcc11c64c9a3b29"
+ "49c1bb60700314611732a6c2fea98eebc0266a11a93970100e",
+ "b3da079b0aa493a5772029f0467baebee5a8112d9d3a22532361da294f7bb381"
+ "5c5dc59e176b4d9f381ca0938e13c6c07b174be65dfa578e80",
+ "64a65f3cdedcdd66811e2915e7",
+ None,
+ "",
+ "6a12066f55331b6c22acd5d5bfc5d71228fbda80ae8dec26bdd306743c5027cb"
+ "4890810c162c027468675ecf645a83176c0d7323a2ccde2d80efe5a1268e8aca"
+ "1d6fbc194d3f77c44986eb4ab4177919ad8bec33eb47bbb5fc6e28196fd1caf5"
+ "6b4e7e0ba5519234d047155ac727a1053100",
+ ),
+ (
+ "d65df341ad13e008567688baedda8e9dcdc17dc024974ea5b4227b6530e339bf"
+ "f21f99e68ca6968f3cca6dfe0fb9f4fab4fa135d5542ea3f01",
+ "df9705f58edbab802c7f8363cfe5560ab1c6132c20a9f1dd163483a26f8ac53a"
+ "39d6808bf4a1dfbd261b099bb03b3fb50906cb28bd8a081f00",
+ "bd0f6a3747cd561bdddf4640a332461a4a30a12a434cd0bf40d766d9c6d458e5"
+ "512204a30c17d1f50b5079631f64eb3112182da3005835461113718d1a5ef944",
+ None,
+ "",
+ "554bc2480860b49eab8532d2a533b7d578ef473eeb58c98bb2d0e1ce488a98b1"
+ "8dfde9b9b90775e67f47d4a1c3482058efc9f40d2ca033a0801b63d45b3b722e"
+ "f552bad3b4ccb667da350192b61c508cf7b6b5adadc2c8d9a446ef003fb05cba"
+ "5f30e88e36ec2703b349ca229c2670833900",
+ ),
+ (
+ "2ec5fe3c17045abdb136a5e6a913e32ab75ae68b53d2fc149b77e504132d3756"
+ "9b7e766ba74a19bd6162343a21c8590aa9cebca9014c636df5",
+ "79756f014dcfe2079f5dd9e718be4171e2ef2486a08f25186f6bff43a9936b9b"
+ "fe12402b08ae65798a3d81e22e9ec80e7690862ef3d4ed3a00",
+ "15777532b0bdd0d1389f636c5f6b9ba734c90af572877e2d272dd078aa1e567c"
+ "fa80e12928bb542330e8409f3174504107ecd5efac61ae7504dabe2a602ede89"
+ "e5cca6257a7c77e27a702b3ae39fc769fc54f2395ae6a1178cab4738e543072f"
+ "c1c177fe71e92e25bf03e4ecb72f47b64d0465aaea4c7fad372536c8ba516a60"
+ "39c3c2a39f0e4d832be432dfa9a706a6e5c7e19f397964ca4258002f7c0541b5"
+ "90316dbc5622b6b2a6fe7a4abffd96105eca76ea7b98816af0748c10df048ce0"
+ "12d901015a51f189f3888145c03650aa23ce894c3bd889e030d565071c59f409"
+ "a9981b51878fd6fc110624dcbcde0bf7a69ccce38fabdf86f3bef6044819de11",
+ None,
+ "",
+ "c650ddbb0601c19ca11439e1640dd931f43c518ea5bea70d3dcde5f4191fe53f"
+ "00cf966546b72bcc7d58be2b9badef28743954e3a44a23f880e8d4f1cfce2d7a"
+ "61452d26da05896f0a50da66a239a8a188b6d825b3305ad77b73fbac0836ecc6"
+ "0987fd08527c1a8e80d5823e65cafe2a3d00",
+ ),
+ (
+ "872d093780f5d3730df7c212664b37b8a0f24f56810daa8382cd4fa3f77634ec"
+ "44dc54f1c2ed9bea86fafb7632d8be199ea165f5ad55dd9ce8",
+ "a81b2e8a70a5ac94ffdbcc9badfc3feb0801f258578bb114ad44ece1ec0e799d"
+ "a08effb81c5d685c0c56f64eecaef8cdf11cc38737838cf400",
+ "6ddf802e1aae4986935f7f981ba3f0351d6273c0a0c22c9c0e8339168e675412"
+ "a3debfaf435ed651558007db4384b650fcc07e3b586a27a4f7a00ac8a6fec2cd"
+ "86ae4bf1570c41e6a40c931db27b2faa15a8cedd52cff7362c4e6e23daec0fbc"
+ "3a79b6806e316efcc7b68119bf46bc76a26067a53f296dafdbdc11c77f7777e9"
+ "72660cf4b6a9b369a6665f02e0cc9b6edfad136b4fabe723d2813db3136cfde9"
+ "b6d044322fee2947952e031b73ab5c603349b307bdc27bc6cb8b8bbd7bd32321"
+ "9b8033a581b59eadebb09b3c4f3d2277d4f0343624acc817804728b25ab79717"
+ "2b4c5c21a22f9c7839d64300232eb66e53f31c723fa37fe387c7d3e50bdf9813"
+ "a30e5bb12cf4cd930c40cfb4e1fc622592a49588794494d56d24ea4b40c89fc0"
+ "596cc9ebb961c8cb10adde976a5d602b1c3f85b9b9a001ed3c6a4d3b1437f520"
+ "96cd1956d042a597d561a596ecd3d1735a8d570ea0ec27225a2c4aaff26306d1"
+ "526c1af3ca6d9cf5a2c98f47e1c46db9a33234cfd4d81f2c98538a09ebe76998"
+ "d0d8fd25997c7d255c6d66ece6fa56f11144950f027795e653008f4bd7ca2dee"
+ "85d8e90f3dc315130ce2a00375a318c7c3d97be2c8ce5b6db41a6254ff264fa6"
+ "155baee3b0773c0f497c573f19bb4f4240281f0b1f4f7be857a4e59d416c06b4"
+ "c50fa09e1810ddc6b1467baeac5a3668d11b6ecaa901440016f389f80acc4db9"
+ "77025e7f5924388c7e340a732e554440e76570f8dd71b7d640b3450d1fd5f041"
+ "0a18f9a3494f707c717b79b4bf75c98400b096b21653b5d217cf3565c9597456"
+ "f70703497a078763829bc01bb1cbc8fa04eadc9a6e3f6699587a9e75c94e5bab"
+ "0036e0b2e711392cff0047d0d6b05bd2a588bc109718954259f1d86678a579a3"
+ "120f19cfb2963f177aeb70f2d4844826262e51b80271272068ef5b3856fa8535"
+ "aa2a88b2d41f2a0e2fda7624c2850272ac4a2f561f8f2f7a318bfd5caf969614"
+ "9e4ac824ad3460538fdc25421beec2cc6818162d06bbed0c40a387192349db67"
+ "a118bada6cd5ab0140ee273204f628aad1c135f770279a651e24d8c14d75a605"
+ "9d76b96a6fd857def5e0b354b27ab937a5815d16b5fae407ff18222c6d1ed263"
+ "be68c95f32d908bd895cd76207ae726487567f9a67dad79abec316f683b17f2d"
+ "02bf07e0ac8b5bc6162cf94697b3c27cd1fea49b27f23ba2901871962506520c"
+ "392da8b6ad0d99f7013fbc06c2c17a569500c8a7696481c1cd33e9b14e40b82e"
+ "79a5f5db82571ba97bae3ad3e0479515bb0e2b0f3bfcd1fd33034efc6245eddd"
+ "7ee2086ddae2600d8ca73e214e8c2b0bdb2b047c6a464a562ed77b73d2d841c4"
+ "b34973551257713b753632efba348169abc90a68f42611a40126d7cb21b58695"
+ "568186f7e569d2ff0f9e745d0487dd2eb997cafc5abf9dd102e62ff66cba87",
+ None,
+ "",
+ "e301345a41a39a4d72fff8df69c98075a0cc082b802fc9b2b6bc503f926b65bd"
+ "df7f4c8f1cb49f6396afc8a70abe6d8aef0db478d4c6b2970076c6a0484fe76d"
+ "76b3a97625d79f1ce240e7c576750d295528286f719b413de9ada3e8eb78ed57"
+ "3603ce30d8bb761785dc30dbc320869e1a00"
+ ),
+ # 7.5 Ed448ph
+ (
+ "833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42"
+ "ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49",
+ "259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743"
+ "c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880",
+ "616263",
+ SHAKE256,
+ "",
+ "822f6901f7480f3d5f562c592994d9693602875614483256505600bbc281ae38"
+ "1f54d6bce2ea911574932f52a4e6cadd78769375ec3ffd1b801a0d9b3f4030cd"
+ "433964b6457ea39476511214f97469b57dd32dbc560a9a94d00bff07620464a3"
+ "ad203df7dc7ce360c3cd3696d9d9fab90f00"
+ ),
+ (
+ "833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42"
+ "ef7822e0d5104127dc05d6dbefde69e3ab2cec7c867c6e2c49",
+ "259b71c19f83ef77a7abd26524cbdb3161b590a48f7d17de3ee0ba9c52beb743"
+ "c09428a131d6b1b57303d90d8132c276d5ed3d5d01c0f53880",
+ "616263",
+ SHAKE256,
+ "666f6f",
+ "c32299d46ec8ff02b54540982814dce9a05812f81962b649d528095916a2aa48"
+ "1065b1580423ef927ecf0af5888f90da0f6a9a85ad5dc3f280d91224ba9911a3"
+ "653d00e484e2ce232521481c8658df304bb7745a73514cdb9bf3e15784ab7128"
+ "4f8d0704a608c54a6b62d97beb511d132100",
+ ),
+)
+
+
+rfc8032_tv_bytes = []
+for tv_str in rfc8032_tv_str:
+ rfc8032_tv_bytes.append([unhexlify(i) if isinstance(i, str) else i for i in tv_str])
+
+
+class TestEdDSA(unittest.TestCase):
+
+ def test_sign(self):
+ for sk, _, msg, hashmod, ctx, exp_signature in rfc8032_tv_bytes:
+ key = eddsa.import_private_key(sk)
+ signer = eddsa.new(key, 'rfc8032', context=ctx)
+ if hashmod is None:
+ # PureEdDSA
+ signature = signer.sign(msg)
+ else:
+ # HashEdDSA
+ hashobj = hashmod.new(msg)
+ signature = signer.sign(hashobj)
+ self.assertEqual(exp_signature, signature)
+
+ def test_verify(self):
+ for _, pk, msg, hashmod, ctx, exp_signature in rfc8032_tv_bytes:
+ key = eddsa.import_public_key(pk)
+ verifier = eddsa.new(key, 'rfc8032', context=ctx)
+ if hashmod is None:
+ # PureEdDSA
+ verifier.verify(msg, exp_signature)
+ else:
+ # HashEdDSA
+ hashobj = hashmod.new(msg)
+ verifier.verify(hashobj, exp_signature)
+
+ def test_double_sign_verify_ed25519(self):
+ msg_hash = SHA512.new(b'abc')
+ key = ECC.generate(curve='ed25519')
+ signer = eddsa.new(key, 'rfc8032')
+ verifier = eddsa.new(key, 'rfc8032')
+
+ signature = signer.sign(msg_hash)
+ signature2 = signer.sign(msg_hash)
+ self.assertEqual(signature, signature2)
+
+ verifier.verify(msg_hash, signature)
+ verifier.verify(msg_hash, signature)
+
+ def test_double_sign_verify_ed448(self):
+ msg_hash = SHAKE256.new(b'abc')
+ key = ECC.generate(curve='ed448')
+ signer = eddsa.new(key, 'rfc8032')
+ verifier = eddsa.new(key, 'rfc8032')
+
+ signature = signer.sign(msg_hash)
+ signature2 = signer.sign(msg_hash)
+ self.assertEqual(signature, signature2)
+
+ verifier.verify(msg_hash, signature)
+ verifier.verify(msg_hash, signature)
+
+ def test_negative(self):
+ key = ECC.generate(curve="ed25519")
+ self.assertRaises(ValueError, eddsa.new, key, 'rfc9999')
+
+ nist_key = ECC.generate(curve="p256")
+ self.assertRaises(ValueError, eddsa.new, nist_key, 'rfc8032')
+
+
+class TestExport_Ed25519(unittest.TestCase):
+
+ def test_raw(self):
+ key = ECC.generate(curve="Ed25519")
+ x, y = key.pointQ.xy
+ raw = bytearray(key._export_eddsa_public())
+ sign_x = raw[31] >> 7
+ raw[31] &= 0x7F
+ yt = bytes_to_long(raw[::-1])
+ self.assertEqual(y, yt)
+ self.assertEqual(x & 1, sign_x)
+
+ key = ECC.construct(point_x=0, point_y=1, curve="Ed25519")
+ out = key._export_eddsa_public()
+ self.assertEqual(b'\x01' + b'\x00' * 31, out)
+
+
+class TestExport_Ed448(unittest.TestCase):
+
+ def test_raw(self):
+ key = ECC.generate(curve="Ed448")
+ x, y = key.pointQ.xy
+ raw = bytearray(key._export_eddsa_public())
+ sign_x = raw[56] >> 7
+ raw[56] &= 0x7F
+ yt = bytes_to_long(raw[::-1])
+ self.assertEqual(y, yt)
+ self.assertEqual(x & 1, sign_x)
+
+ key = ECC.construct(point_x=0, point_y=1, curve="Ed448")
+ out = key._export_eddsa_public()
+ self.assertEqual(b'\x01' + b'\x00' * 56, out)
+
+
+class TestImport_Ed25519(unittest.TestCase):
+
+ def test_raw(self):
+ Px = 24407857220263921307776619664228778204996144802740950419837658238229122415920
+ Py = 56480760040633817885061096979765646085062883740629155052073094891081309750690
+ encoded = b'\xa2\x05\xd6\x00\xe1 \xe1\xc0\xff\x96\xee?V\x8e\xba/\xd3\x89\x06\xd7\xc4c\xe8$\xc2d\xd7a1\xfa\xde|'
+ key = eddsa.import_public_key(encoded)
+ self.assertEqual(Py, key.pointQ.y)
+ self.assertEqual(Px, key.pointQ.x)
+
+ encoded = b'\x01' + b'\x00' * 31
+ key = eddsa.import_public_key(encoded)
+ self.assertEqual(1, key.pointQ.y)
+ self.assertEqual(0, key.pointQ.x)
+
+
+class TestImport_Ed448(unittest.TestCase):
+
+ def test_raw(self):
+ Px = 0x153f42025aba3b0daecaa5cd79458b3146c7c9378c16c17b4a59bc3561113d90c169045bc12966c3f93e140c2ca0a3acc33d9205b9daf9b1
+ Py = 0x38f5c0015d3dedd576c232810dd90373b5b1d631a12894c043b7be529cbae03ede177d8fa490b56131dbcb2465d2aba777ef839fc1719b25
+ encoded = unhexlify("259b71c19f83ef77a7abd26524cbdb31"
+ "61b590a48f7d17de3ee0ba9c52beb743"
+ "c09428a131d6b1b57303d90d8132c276"
+ "d5ed3d5d01c0f53880")
+ key = eddsa.import_public_key(encoded)
+ self.assertEqual(Py, key.pointQ.y)
+ self.assertEqual(Px, key.pointQ.x)
+
+ encoded = b'\x01' + b'\x00' * 56
+ key = eddsa.import_public_key(encoded)
+ self.assertEqual(1, key.pointQ.y)
+ self.assertEqual(0, key.pointQ.x)
+
+
+class TestVectorsEdDSAWycheproof(unittest.TestCase):
+
+ def add_tests(self, filename):
+
+ def pk(group):
+ elem = group['key']['pk']
+ return unhexlify(elem)
+
+ def sk(group):
+ elem = group['key']['sk']
+ return unhexlify(elem)
+
+ result = load_test_vectors_wycheproof(("Signature", "wycheproof"),
+ filename,
+ "Wycheproof ECDSA signature (%s)"
+ % filename,
+ group_tag={'pk': pk, 'sk': sk})
+ self.tv += result
+
+ def setUp(self):
+ self.tv = []
+ self.add_tests("eddsa_test.json")
+ self.add_tests("ed448_test.json")
+
+ def test_sign(self, tv):
+ if not tv.valid:
+ return
+
+ self._id = "Wycheproof EdDSA Sign Test #%d (%s, %s)" % (tv.id, tv.comment, tv.filename)
+ key = eddsa.import_private_key(tv.sk)
+ signer = eddsa.new(key, 'rfc8032')
+ signature = signer.sign(tv.msg)
+ self.assertEqual(signature, tv.sig)
+
+ def test_verify(self, tv):
+ self._id = "Wycheproof EdDSA Verify Test #%d (%s, %s)" % (tv.id, tv.comment, tv.filename)
+ key = eddsa.import_public_key(tv.pk)
+ verifier = eddsa.new(key, 'rfc8032')
+ try:
+ verifier.verify(tv.msg, tv.sig)
+ except ValueError:
+ assert not tv.valid
+ else:
+ assert tv.valid
+
+ def runTest(self):
+ for tv in self.tv:
+ self.test_sign(tv)
+ self.test_verify(tv)
+
+
+def get_tests(config={}):
+
+ tests = []
+ tests += list_test_cases(TestExport_Ed25519)
+ tests += list_test_cases(TestExport_Ed448)
+ tests += list_test_cases(TestImport_Ed25519)
+ tests += list_test_cases(TestImport_Ed448)
+ tests += list_test_cases(TestEdDSA)
+ tests += [TestVectorsEdDSAWycheproof()]
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_pkcs1_15.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_pkcs1_15.py
new file mode 100644
index 00000000..8e2c6ee6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_pkcs1_15.py
@@ -0,0 +1,348 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 json
+import unittest
+from binascii import unhexlify
+
+from Crypto.Util.py3compat import bchr
+from Crypto.Util.number import bytes_to_long
+from Crypto.Util.strxor import strxor
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors, load_test_vectors_wycheproof
+
+from Crypto.Hash import (SHA1, SHA224, SHA256, SHA384, SHA512, SHA3_384,
+ SHA3_224, SHA3_256, SHA3_512)
+from Crypto.PublicKey import RSA
+from Crypto.Signature import pkcs1_15
+from Crypto.Signature import PKCS1_v1_5
+
+from Crypto.Util._file_system import pycryptodome_filename
+from Crypto.Util.strxor import strxor
+
+
+def load_hash_by_name(hash_name):
+ return __import__("Crypto.Hash." + hash_name, globals(), locals(), ["new"])
+
+
+class FIPS_PKCS1_Verify_Tests(unittest.TestCase):
+
+ def shortDescription(self):
+ return "FIPS PKCS1 Tests (Verify)"
+
+ def test_can_sign(self):
+ test_public_key = RSA.generate(1024).public_key()
+ verifier = pkcs1_15.new(test_public_key)
+ self.assertEqual(verifier.can_sign(), False)
+
+
+class FIPS_PKCS1_Verify_Tests_KAT(unittest.TestCase):
+ pass
+
+
+test_vectors_verify = load_test_vectors(("Signature", "PKCS1-v1.5"),
+ "SigVer15_186-3.rsp",
+ "Signature Verification 186-3",
+ {'shaalg': lambda x: x,
+ 'd': lambda x: int(x),
+ 'result': lambda x: x}) or []
+
+
+for count, tv in enumerate(test_vectors_verify):
+ if isinstance(tv, str):
+ continue
+ if hasattr(tv, "n"):
+ modulus = tv.n
+ continue
+
+ hash_module = load_hash_by_name(tv.shaalg.upper())
+ hash_obj = hash_module.new(tv.msg)
+ public_key = RSA.construct([bytes_to_long(x) for x in (modulus, tv.e)]) # type: ignore
+ verifier = pkcs1_15.new(public_key)
+
+ def positive_test(self, hash_obj=hash_obj, verifier=verifier, signature=tv.s):
+ verifier.verify(hash_obj, signature)
+
+ def negative_test(self, hash_obj=hash_obj, verifier=verifier, signature=tv.s):
+ self.assertRaises(ValueError, verifier.verify, hash_obj, signature)
+
+ if tv.result == 'f':
+ setattr(FIPS_PKCS1_Verify_Tests_KAT, "test_negative_%d" % count, negative_test)
+ else:
+ setattr(FIPS_PKCS1_Verify_Tests_KAT, "test_positive_%d" % count, positive_test)
+
+
+class FIPS_PKCS1_Sign_Tests(unittest.TestCase):
+
+ def shortDescription(self):
+ return "FIPS PKCS1 Tests (Sign)"
+
+ def test_can_sign(self):
+ test_private_key = RSA.generate(1024)
+ signer = pkcs1_15.new(test_private_key)
+ self.assertEqual(signer.can_sign(), True)
+
+
+class FIPS_PKCS1_Sign_Tests_KAT(unittest.TestCase):
+ pass
+
+
+test_vectors_sign = load_test_vectors(("Signature", "PKCS1-v1.5"),
+ "SigGen15_186-2.txt",
+ "Signature Generation 186-2",
+ {'shaalg': lambda x: x}) or []
+
+test_vectors_sign += load_test_vectors(("Signature", "PKCS1-v1.5"),
+ "SigGen15_186-3.txt",
+ "Signature Generation 186-3",
+ {'shaalg': lambda x: x}) or []
+
+for count, tv in enumerate(test_vectors_sign):
+ if isinstance(tv, str):
+ continue
+ if hasattr(tv, "n"):
+ modulus = tv.n
+ continue
+ if hasattr(tv, "e"):
+ private_key = RSA.construct([bytes_to_long(x) for x in (modulus, tv.e, tv.d)]) # type: ignore
+ signer = pkcs1_15.new(private_key)
+ continue
+
+ hash_module = load_hash_by_name(tv.shaalg.upper())
+ hash_obj = hash_module.new(tv.msg)
+
+ def new_test(self, hash_obj=hash_obj, signer=signer, result=tv.s):
+ signature = signer.sign(hash_obj)
+ self.assertEqual(signature, result)
+
+ setattr(FIPS_PKCS1_Sign_Tests_KAT, "test_%d" % count, new_test)
+
+
+class PKCS1_15_NoParams(unittest.TestCase):
+ """Verify that PKCS#1 v1.5 signatures pass even without NULL parameters in
+ the algorithm identifier (PyCrypto/LP bug #1119552)."""
+
+ rsakey = """-----BEGIN RSA PRIVATE KEY-----
+ MIIBOwIBAAJBAL8eJ5AKoIsjURpcEoGubZMxLD7+kT+TLr7UkvEtFrRhDDKMtuII
+ q19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQJACUSDEp8RTe32ftq8IwG8
+ Wojl5mAd1wFiIOrZ/Uv8b963WJOJiuQcVN29vxU5+My9GPZ7RA3hrDBEAoHUDPrI
+ OQIhAPIPLz4dphiD9imAkivY31Rc5AfHJiQRA7XixTcjEkojAiEAyh/pJHks/Mlr
+ +rdPNEpotBjfV4M4BkgGAA/ipcmaAjcCIQCHvhwwKVBLzzTscT2HeUdEeBMoiXXK
+ JACAr3sJQJGxIQIgarRp+m1WSKV1MciwMaTOnbU7wxFs9DP1pva76lYBzgUCIQC9
+ n0CnZCJ6IZYqSt0H5N7+Q+2Ro64nuwV/OSQfM6sBwQ==
+ -----END RSA PRIVATE KEY-----"""
+
+ msg = b"This is a test\x0a"
+
+ # PKCS1 v1.5 signature of the message computed using SHA-1.
+ # The digestAlgorithm SEQUENCE does NOT contain the NULL parameter.
+ sig_str = "a287a13517f716e72fb14eea8e33a8db4a4643314607e7ca3e3e28"\
+ "1893db74013dda8b855fd99f6fecedcb25fcb7a434f35cd0a101f8"\
+ "b19348e0bd7b6f152dfc"
+ signature = unhexlify(sig_str)
+
+ def runTest(self):
+ verifier = pkcs1_15.new(RSA.importKey(self.rsakey))
+ hashed = SHA1.new(self.msg)
+ verifier.verify(hashed, self.signature)
+
+
+class PKCS1_Legacy_Module_Tests(unittest.TestCase):
+ """Verify that the legacy module Crypto.Signature.PKCS1_v1_5
+ behaves as expected. The only difference is that the verify()
+ method returns True/False and does not raise exceptions."""
+
+ def shortDescription(self):
+ return "Test legacy Crypto.Signature.PKCS1_v1_5"
+
+ def runTest(self):
+ key = RSA.importKey(PKCS1_15_NoParams.rsakey)
+ hashed = SHA1.new(b"Test")
+ good_signature = PKCS1_v1_5.new(key).sign(hashed)
+ verifier = PKCS1_v1_5.new(key.public_key())
+
+ self.assertEqual(verifier.verify(hashed, good_signature), True)
+
+ # Flip a few bits in the signature
+ bad_signature = strxor(good_signature, bchr(1) * len(good_signature))
+ self.assertEqual(verifier.verify(hashed, bad_signature), False)
+
+
+class PKCS1_All_Hashes_Tests(unittest.TestCase):
+
+ def shortDescription(self):
+ return "Test PKCS#1v1.5 signature in combination with all hashes"
+
+ def runTest(self):
+
+ key = RSA.generate(1024)
+ signer = pkcs1_15.new(key)
+ hash_names = ("MD2", "MD4", "MD5", "RIPEMD160", "SHA1",
+ "SHA224", "SHA256", "SHA384", "SHA512",
+ "SHA3_224", "SHA3_256", "SHA3_384", "SHA3_512")
+
+ for name in hash_names:
+ hashed = load_hash_by_name(name).new(b"Test")
+ signer.sign(hashed)
+
+ from Crypto.Hash import BLAKE2b, BLAKE2s
+ for hash_size in (20, 32, 48, 64):
+ hashed_b = BLAKE2b.new(digest_bytes=hash_size, data=b"Test")
+ signer.sign(hashed_b)
+ for hash_size in (16, 20, 28, 32):
+ hashed_s = BLAKE2s.new(digest_bytes=hash_size, data=b"Test")
+ signer.sign(hashed_s)
+
+
+class TestVectorsWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._id = "None"
+
+ def setUp(self):
+ self.tv = []
+ self.add_tests("rsa_sig_gen_misc_test.json")
+ self.add_tests("rsa_signature_2048_sha224_test.json")
+ self.add_tests("rsa_signature_2048_sha256_test.json")
+ self.add_tests("rsa_signature_2048_sha384_test.json")
+ self.add_tests("rsa_signature_2048_sha3_224_test.json")
+ self.add_tests("rsa_signature_2048_sha3_256_test.json")
+ self.add_tests("rsa_signature_2048_sha3_384_test.json")
+ self.add_tests("rsa_signature_2048_sha3_512_test.json")
+ self.add_tests("rsa_signature_2048_sha512_test.json")
+ self.add_tests("rsa_signature_2048_sha512_224_test.json")
+ self.add_tests("rsa_signature_2048_sha512_256_test.json")
+ self.add_tests("rsa_signature_3072_sha256_test.json")
+ self.add_tests("rsa_signature_3072_sha384_test.json")
+ self.add_tests("rsa_signature_3072_sha3_256_test.json")
+ self.add_tests("rsa_signature_3072_sha3_384_test.json")
+ self.add_tests("rsa_signature_3072_sha3_512_test.json")
+ self.add_tests("rsa_signature_3072_sha512_test.json")
+ self.add_tests("rsa_signature_3072_sha512_256_test.json")
+ self.add_tests("rsa_signature_4096_sha384_test.json")
+ self.add_tests("rsa_signature_4096_sha512_test.json")
+ self.add_tests("rsa_signature_4096_sha512_256_test.json")
+ self.add_tests("rsa_signature_test.json")
+
+ def add_tests(self, filename):
+
+ def filter_rsa(group):
+ return RSA.import_key(group['keyPem'])
+
+ def filter_sha(group):
+ hash_name = group['sha']
+ if hash_name == "SHA-512":
+ return SHA512
+ elif hash_name == "SHA-512/224":
+ return SHA512.new(truncate="224")
+ elif hash_name == "SHA-512/256":
+ return SHA512.new(truncate="256")
+ elif hash_name == "SHA3-512":
+ return SHA3_512
+ elif hash_name == "SHA-384":
+ return SHA384
+ elif hash_name == "SHA3-384":
+ return SHA3_384
+ elif hash_name == "SHA-256":
+ return SHA256
+ elif hash_name == "SHA3-256":
+ return SHA3_256
+ elif hash_name == "SHA-224":
+ return SHA224
+ elif hash_name == "SHA3-224":
+ return SHA3_224
+ elif hash_name == "SHA-1":
+ return SHA1
+ else:
+ raise ValueError("Unknown hash algorithm: " + hash_name)
+
+ def filter_type(group):
+ type_name = group['type']
+ if type_name not in ("RsassaPkcs1Verify", "RsassaPkcs1Generate"):
+ raise ValueError("Unknown type name " + type_name)
+
+ result = load_test_vectors_wycheproof(("Signature", "wycheproof"),
+ filename,
+ "Wycheproof PKCS#1v1.5 signature (%s)" % filename,
+ group_tag={'rsa_key': filter_rsa,
+ 'hash_mod': filter_sha,
+ 'type': filter_type})
+ return result
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_verify(self, tv):
+ self._id = "Wycheproof RSA PKCS$#1 Test #" + str(tv.id)
+
+ hashed_msg = tv.hash_module.new(tv.msg)
+ signer = pkcs1_15.new(tv.key)
+ try:
+ signature = signer.verify(hashed_msg, tv.sig)
+ except ValueError as e:
+ if tv.warning:
+ return
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.warn(tv)
+
+ def runTest(self):
+ for tv in self.tv:
+ self.test_verify(tv)
+
+
+def get_tests(config={}):
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ tests = []
+ tests += list_test_cases(FIPS_PKCS1_Verify_Tests)
+ tests += list_test_cases(FIPS_PKCS1_Sign_Tests)
+ tests += list_test_cases(PKCS1_15_NoParams)
+ tests += list_test_cases(PKCS1_Legacy_Module_Tests)
+ tests += list_test_cases(PKCS1_All_Hashes_Tests)
+ tests += [ TestVectorsWycheproof(wycheproof_warnings) ]
+
+ if config.get('slow_tests'):
+ tests += list_test_cases(FIPS_PKCS1_Verify_Tests_KAT)
+ tests += list_test_cases(FIPS_PKCS1_Sign_Tests_KAT)
+
+ return tests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_pss.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_pss.py
new file mode 100644
index 00000000..535474be
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Signature/test_pss.py
@@ -0,0 +1,377 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 unittest
+
+from Crypto.Util.py3compat import b, bchr
+from Crypto.Util.number import bytes_to_long
+from Crypto.Util.strxor import strxor
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.SelfTest.loader import load_test_vectors, load_test_vectors_wycheproof
+
+from Crypto.Hash import SHA1, SHA224, SHA256, SHA384, SHA512
+from Crypto.PublicKey import RSA
+from Crypto.Signature import pss
+from Crypto.Signature import PKCS1_PSS
+
+from Crypto.Signature.pss import MGF1
+
+
+def load_hash_by_name(hash_name):
+ return __import__("Crypto.Hash." + hash_name, globals(), locals(), ["new"])
+
+
+class PRNG(object):
+
+ def __init__(self, stream):
+ self.stream = stream
+ self.idx = 0
+
+ def __call__(self, rnd_size):
+ result = self.stream[self.idx:self.idx + rnd_size]
+ self.idx += rnd_size
+ return result
+
+
+class PSS_Tests(unittest.TestCase):
+
+ rsa_key = b'-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAsvI34FgiTK8+txBvmooNGpNwk23YTU51dwNZi5yha3W4lA/Q\nvcZrDalkmD7ekWQwnduxVKa6pRSI13KBgeUOIqJoGXSWhntEtY3FEwvWOHW5AE7Q\njUzTzCiYT6TVaCcpa/7YLai+p6ai2g5f5Zfh4jSawa9uYeuggFygQq4IVW796MgV\nyqxYMM/arEj+/sKz3Viua9Rp9fFosertCYCX4DUTgW0mX9bwEnEOgjSI3pLOPXz1\n8vx+DRZS5wMCmwCUa0sKonLn3cAUPq+sGix7+eo7T0Z12MU8ud7IYVX/75r3cXiF\nPaYE2q8Le0kgOApIXbb+x74x0rNgyIh1yGygkwIDAQABAoIBABz4t1A0pLT6qHI2\nEIOaNz3mwhK0dZEqkz0GB1Dhtoax5ATgvKCFB98J3lYB08IBURe1snOsnMpOVUtg\naBRSM+QqnCUG6bnzKjAkuFP5liDE+oNQv1YpKp9CsUovuzdmI8Au3ewihl+ZTIN2\nUVNYMEOR1b5m+z2SSwWNOYsiJwpBrT7zkpdlDyjat7FiiPhMMIMXjhQFVxURMIcB\njUBtPzGvV/PG90cVDWi1wRGeeP1dDqti/jsnvykQ15KW1MqGrpeNKRmDdTy/Ucl1\nWIoYklKw3U456lgZ/rDTDB818+Tlnk35z4yF7d5ANPM8CKfqOPcnO1BCKVFzf4eq\n54wvUtkCgYEA1Zv2lp06l7rXMsvNtyYQjbFChezRDRnPwZmN4NCdRtTgGG1G0Ryd\nYz6WWoPGqZp0b4LAaaHd3W2GTcpXF8WXMKfMX1W+tMAxMozfsXRKMcHoypwuS5wT\nfJRXJCG4pvd57AB0iVUEJW2we+uGKU5Zxcx//id2nXGCpoRyViIplQsCgYEA1nVC\neHupHChht0Fh4N09cGqZHZzuwXjOUMzR3Vsfz+4WzVS3NvIgN4g5YgmQFOeKwo5y\niRq5yvubcNdFvf85eHWClg0zPAyxJCVUWigCrrOanGEhJo6re4idJvNVzu4Ucg0v\n6B3SJ1HsCda+ZSNz24bSyqRep8A+RoAaoVSFx5kCgYEAn3RvXPs9s+obnqWYiPF3\nRe5etE6Vt2vfNKwFxx6zaR6bsmBQjuUHcABWiHb6I71S0bMPI0tbrWGG8ibrYKl1\nNTLtUvVVCOS3VP7oNTWT9RTFTAnOXU7DFSo+6o/poWn3r36ff6zhDXeWWMr2OXtt\ndEQ1/2lCGEGVv+v61eVmmQUCgYABFHITPTwqwiFL1O5zPWnzyPWgaovhOYSAb6eW\n38CXQXGn8wdBJZL39J2lWrr4//l45VK6UgIhfYbY2JynSkO10ZGow8RARygVMILu\nOUlaK9lZdDvAf/NpGdUAvzTtZ9F+iYZ2OsA2JnlzyzsGM1l//3vMPWukmJk3ral0\nqoJJ8QKBgGRG3eVHnIegBbFVuMDp2NTcfuSuDVUQ1fGAwtPiFa8u81IodJnMk2pq\niXu2+0ytNA/M+SVrAnE2AgIzcaJbtr0p2srkuVM7KMWnG1vWFNjtXN8fAhf/joOv\nD+NmPL/N4uE57e40tbiU/H7KdyZaDt+5QiTmdhuyAe6CBjKsF2jy\n-----END RSA PRIVATE KEY-----'
+ msg = b'AAA'
+ tag = b'\x00[c5\xd8\xb0\x8b!D\x81\x83\x07\xc0\xdd\xb9\xb4\xb2`\x92\xe7\x02\xf1\xe1P\xea\xc3\xf0\xe3>\xddX5\xdd\x8e\xc5\x89\xef\xf3\xc2\xdc\xfeP\x02\x7f\x12+\xc9\xaf\xbb\xec\xfe\xb0\xa5\xb9\x08\x11P\x8fL\xee5\x9b\xb0k{=_\xd2\x14\xfb\x01R\xb7\xfe\x14}b\x03\x8d5Y\x89~}\xfc\xf2l\xd01-\xbd\xeb\x11\xcdV\x11\xe9l\x19k/o5\xa2\x0f\x15\xe7Q$\t=\xec\x1dAB\x19\xa5P\x9a\xaf\xa3G\x86"\xd6~\xf0j\xfcqkbs\x13\x84b\xe4\xbdm(\xed`\xa4F\xfb\x8f.\xe1\x8c)/_\x9eS\x98\xa4v\xb8\xdc\xfe\xf7/D\x18\x19\xb3T\x97:\xe2\x96s\xe8<\xa2\xb4\xb9\xf8/'
+
+ def test_positive_1(self):
+ key = RSA.import_key(self.rsa_key)
+ h = SHA256.new(self.msg)
+ verifier = pss.new(key)
+ verifier.verify(h, self.tag)
+
+ def test_negative_1(self):
+ key = RSA.import_key(self.rsa_key)
+ h = SHA256.new(self.msg + b'A')
+ verifier = pss.new(key)
+ tag = bytearray(self.tag)
+ self.assertRaises(ValueError, verifier.verify, h, tag)
+
+ def test_negative_2(self):
+ key = RSA.import_key(self.rsa_key)
+ h = SHA256.new(self.msg)
+ verifier = pss.new(key, salt_bytes=1000)
+ tag = bytearray(self.tag)
+ self.assertRaises(ValueError, verifier.verify, h, tag)
+
+
+class FIPS_PKCS1_Verify_Tests(unittest.TestCase):
+
+ def shortDescription(self):
+ return "FIPS PKCS1 Tests (Verify)"
+
+ def verify_positive(self, hashmod, message, public_key, salt, signature):
+ prng = PRNG(salt)
+ hashed = hashmod.new(message)
+ verifier = pss.new(public_key, salt_bytes=len(salt), rand_func=prng)
+ verifier.verify(hashed, signature)
+
+ def verify_negative(self, hashmod, message, public_key, salt, signature):
+ prng = PRNG(salt)
+ hashed = hashmod.new(message)
+ verifier = pss.new(public_key, salt_bytes=len(salt), rand_func=prng)
+ self.assertRaises(ValueError, verifier.verify, hashed, signature)
+
+ def test_can_sign(self):
+ test_public_key = RSA.generate(1024).public_key()
+ verifier = pss.new(test_public_key)
+ self.assertEqual(verifier.can_sign(), False)
+
+
+class FIPS_PKCS1_Verify_Tests_KAT(unittest.TestCase):
+ pass
+
+
+test_vectors_verify = load_test_vectors(("Signature", "PKCS1-PSS"),
+ "SigVerPSS_186-3.rsp",
+ "Signature Verification 186-3",
+ {'shaalg': lambda x: x,
+ 'result': lambda x: x}) or []
+
+
+for count, tv in enumerate(test_vectors_verify):
+ if isinstance(tv, str):
+ continue
+ if hasattr(tv, "n"):
+ modulus = tv.n
+ continue
+ if hasattr(tv, "p"):
+ continue
+
+ hash_module = load_hash_by_name(tv.shaalg.upper())
+ hash_obj = hash_module.new(tv.msg)
+ public_key = RSA.construct([bytes_to_long(x) for x in (modulus, tv.e)]) # type: ignore
+ if tv.saltval != b("\x00"):
+ prng = PRNG(tv.saltval)
+ verifier = pss.new(public_key, salt_bytes=len(tv.saltval), rand_func=prng)
+ else:
+ verifier = pss.new(public_key, salt_bytes=0)
+
+ def positive_test(self, hash_obj=hash_obj, verifier=verifier, signature=tv.s):
+ verifier.verify(hash_obj, signature)
+
+ def negative_test(self, hash_obj=hash_obj, verifier=verifier, signature=tv.s):
+ self.assertRaises(ValueError, verifier.verify, hash_obj, signature)
+
+ if tv.result == 'p':
+ setattr(FIPS_PKCS1_Verify_Tests_KAT, "test_positive_%d" % count, positive_test)
+ else:
+ setattr(FIPS_PKCS1_Verify_Tests_KAT, "test_negative_%d" % count, negative_test)
+
+
+class FIPS_PKCS1_Sign_Tests(unittest.TestCase):
+
+ def shortDescription(self):
+ return "FIPS PKCS1 Tests (Sign)"
+
+ def test_can_sign(self):
+ test_private_key = RSA.generate(1024)
+ signer = pss.new(test_private_key)
+ self.assertEqual(signer.can_sign(), True)
+
+
+class FIPS_PKCS1_Sign_Tests_KAT(unittest.TestCase):
+ pass
+
+
+test_vectors_sign = load_test_vectors(("Signature", "PKCS1-PSS"),
+ "SigGenPSS_186-2.txt",
+ "Signature Generation 186-2",
+ {'shaalg': lambda x: x}) or []
+
+test_vectors_sign += load_test_vectors(("Signature", "PKCS1-PSS"),
+ "SigGenPSS_186-3.txt",
+ "Signature Generation 186-3",
+ {'shaalg': lambda x: x}) or []
+
+for count, tv in enumerate(test_vectors_sign):
+ if isinstance(tv, str):
+ continue
+ if hasattr(tv, "n"):
+ modulus = tv.n
+ continue
+ if hasattr(tv, "e"):
+ private_key = RSA.construct([bytes_to_long(x) for x in (modulus, tv.e, tv.d)]) # type: ignore
+ continue
+
+ hash_module = load_hash_by_name(tv.shaalg.upper())
+ hash_obj = hash_module.new(tv.msg)
+ if tv.saltval != b("\x00"):
+ prng = PRNG(tv.saltval)
+ signer = pss.new(private_key, salt_bytes=len(tv.saltval), rand_func=prng)
+ else:
+ signer = pss.new(private_key, salt_bytes=0)
+
+ def new_test(self, hash_obj=hash_obj, signer=signer, result=tv.s):
+ signature = signer.sign(hash_obj)
+ self.assertEqual(signature, result)
+
+ setattr(FIPS_PKCS1_Sign_Tests_KAT, "test_%d" % count, new_test)
+
+
+class PKCS1_Legacy_Module_Tests(unittest.TestCase):
+ """Verify that the legacy module Crypto.Signature.PKCS1_PSS
+ behaves as expected. The only difference is that the verify()
+ method returns True/False and does not raise exceptions."""
+
+ def shortDescription(self):
+ return "Test legacy Crypto.Signature.PKCS1_PSS"
+
+ def runTest(self):
+ key = RSA.generate(1024)
+ hashed = SHA1.new(b("Test"))
+ good_signature = PKCS1_PSS.new(key).sign(hashed)
+ verifier = PKCS1_PSS.new(key.public_key())
+
+ self.assertEqual(verifier.verify(hashed, good_signature), True)
+
+ # Flip a few bits in the signature
+ bad_signature = strxor(good_signature, bchr(1) * len(good_signature))
+ self.assertEqual(verifier.verify(hashed, bad_signature), False)
+
+
+class PKCS1_All_Hashes_Tests(unittest.TestCase):
+
+ def shortDescription(self):
+ return "Test PKCS#1 PSS signature in combination with all hashes"
+
+ def runTest(self):
+
+ key = RSA.generate(1280)
+ signer = pss.new(key)
+ hash_names = ("MD2", "MD4", "MD5", "RIPEMD160", "SHA1",
+ "SHA224", "SHA256", "SHA384", "SHA512",
+ "SHA3_224", "SHA3_256", "SHA3_384", "SHA3_512")
+
+ for name in hash_names:
+ hashed = load_hash_by_name(name).new(b("Test"))
+ signer.sign(hashed)
+
+ from Crypto.Hash import BLAKE2b, BLAKE2s
+ for hash_size in (20, 32, 48, 64):
+ hashed_b = BLAKE2b.new(digest_bytes=hash_size, data=b("Test"))
+ signer.sign(hashed_b)
+ for hash_size in (16, 20, 28, 32):
+ hashed_s = BLAKE2s.new(digest_bytes=hash_size, data=b("Test"))
+ signer.sign(hashed_s)
+
+
+def get_hash_module(hash_name):
+ if hash_name == "SHA-512":
+ hash_module = SHA512
+ elif hash_name == "SHA-512/224":
+ hash_module = SHA512.new(truncate="224")
+ elif hash_name == "SHA-512/256":
+ hash_module = SHA512.new(truncate="256")
+ elif hash_name == "SHA-384":
+ hash_module = SHA384
+ elif hash_name == "SHA-256":
+ hash_module = SHA256
+ elif hash_name == "SHA-224":
+ hash_module = SHA224
+ elif hash_name == "SHA-1":
+ hash_module = SHA1
+ else:
+ raise ValueError("Unknown hash algorithm: " + hash_name)
+ return hash_module
+
+
+class TestVectorsPSSWycheproof(unittest.TestCase):
+
+ def __init__(self, wycheproof_warnings):
+ unittest.TestCase.__init__(self)
+ self._wycheproof_warnings = wycheproof_warnings
+ self._id = "None"
+
+ def add_tests(self, filename):
+
+ def filter_rsa(group):
+ return RSA.import_key(group['keyPem'])
+
+ def filter_sha(group):
+ return get_hash_module(group['sha'])
+
+ def filter_type(group):
+ type_name = group['type']
+ if type_name not in ("RsassaPssVerify", ):
+ raise ValueError("Unknown type name " + type_name)
+
+ def filter_slen(group):
+ return group['sLen']
+
+ def filter_mgf(group):
+ mgf = group['mgf']
+ if mgf not in ("MGF1", ):
+ raise ValueError("Unknown MGF " + mgf)
+ mgf1_hash = get_hash_module(group['mgfSha'])
+
+ def mgf(x, y, mh=mgf1_hash):
+ return MGF1(x, y, mh)
+
+ return mgf
+
+ result = load_test_vectors_wycheproof(("Signature", "wycheproof"),
+ filename,
+ "Wycheproof PSS signature (%s)" % filename,
+ group_tag={'key': filter_rsa,
+ 'hash_module': filter_sha,
+ 'sLen': filter_slen,
+ 'mgf': filter_mgf,
+ 'type': filter_type})
+ return result
+
+ def setUp(self):
+ self.tv = []
+ self.add_tests("rsa_pss_2048_sha1_mgf1_20_test.json")
+ self.add_tests("rsa_pss_2048_sha256_mgf1_0_test.json")
+ self.add_tests("rsa_pss_2048_sha256_mgf1_32_test.json")
+ self.add_tests("rsa_pss_2048_sha512_256_mgf1_28_test.json")
+ self.add_tests("rsa_pss_2048_sha512_256_mgf1_32_test.json")
+ self.add_tests("rsa_pss_3072_sha256_mgf1_32_test.json")
+ self.add_tests("rsa_pss_4096_sha256_mgf1_32_test.json")
+ self.add_tests("rsa_pss_4096_sha512_mgf1_32_test.json")
+ self.add_tests("rsa_pss_misc_test.json")
+
+ def shortDescription(self):
+ return self._id
+
+ def warn(self, tv):
+ if tv.warning and self._wycheproof_warnings:
+ import warnings
+ warnings.warn("Wycheproof warning: %s (%s)" % (self._id, tv.comment))
+
+ def test_verify(self, tv):
+ self._id = "Wycheproof RSA PSS Test #%d (%s)" % (tv.id, tv.comment)
+
+ hashed_msg = tv.hash_module.new(tv.msg)
+ signer = pss.new(tv.key, mask_func=tv.mgf, salt_bytes=tv.sLen)
+ try:
+ signature = signer.verify(hashed_msg, tv.sig)
+ except ValueError as e:
+ if tv.warning:
+ return
+ assert not tv.valid
+ else:
+ assert tv.valid
+ self.warn(tv)
+
+ def runTest(self):
+ for tv in self.tv:
+ self.test_verify(tv)
+
+
+def get_tests(config={}):
+ wycheproof_warnings = config.get('wycheproof_warnings')
+
+ tests = []
+ tests += list_test_cases(PSS_Tests)
+ tests += list_test_cases(FIPS_PKCS1_Verify_Tests)
+ tests += list_test_cases(FIPS_PKCS1_Sign_Tests)
+ tests += list_test_cases(PKCS1_Legacy_Module_Tests)
+ tests += list_test_cases(PKCS1_All_Hashes_Tests)
+
+ if config.get('slow_tests'):
+ tests += list_test_cases(FIPS_PKCS1_Verify_Tests_KAT)
+ tests += list_test_cases(FIPS_PKCS1_Sign_Tests_KAT)
+
+ tests += [TestVectorsPSSWycheproof(wycheproof_warnings)]
+
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/__init__.py
new file mode 100644
index 00000000..ee993db6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/__init__.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Util/__init__.py: Self-test for utility modules
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-test for utility modules"""
+
+__revision__ = "$Id$"
+
+import os
+
+def get_tests(config={}):
+ tests = []
+ from Crypto.SelfTest.Util import test_number; tests += test_number.get_tests(config=config)
+ from Crypto.SelfTest.Util import test_Counter; tests += test_Counter.get_tests(config=config)
+ from Crypto.SelfTest.Util import test_Padding; tests += test_Padding.get_tests(config=config)
+ from Crypto.SelfTest.Util import test_strxor; tests += test_strxor.get_tests(config=config)
+ from Crypto.SelfTest.Util import test_asn1; tests += test_asn1.get_tests(config=config)
+ from Crypto.SelfTest.Util import test_rfc1751; tests += test_rfc1751.get_tests(config=config)
+ return tests
+
+if __name__ == '__main__':
+ import unittest
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_Counter.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_Counter.py
new file mode 100644
index 00000000..8837a32c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_Counter.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Util/test_Counter: Self-test for the Crypto.Util.Counter module
+#
+# Written in 2009 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-tests for Crypto.Util.Counter"""
+
+from Crypto.Util.py3compat import *
+
+import unittest
+
+class CounterTests(unittest.TestCase):
+ def setUp(self):
+ global Counter
+ from Crypto.Util import Counter
+
+ def test_BE(self):
+ """Big endian"""
+ c = Counter.new(128)
+ c = Counter.new(128, little_endian=False)
+
+ def test_LE(self):
+ """Little endian"""
+ c = Counter.new(128, little_endian=True)
+
+ def test_nbits(self):
+ c = Counter.new(nbits=128)
+ self.assertRaises(ValueError, Counter.new, 129)
+
+ def test_prefix(self):
+ c = Counter.new(128, prefix=b("xx"))
+
+ def test_suffix(self):
+ c = Counter.new(128, suffix=b("xx"))
+
+ def test_iv(self):
+ c = Counter.new(128, initial_value=2)
+ self.assertRaises(ValueError, Counter.new, 16, initial_value=0x1FFFF)
+
+def get_tests(config={}):
+ from Crypto.SelfTest.st_common import list_test_cases
+ return list_test_cases(CounterTests)
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_Padding.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_Padding.py
new file mode 100644
index 00000000..0929fd47
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_Padding.py
@@ -0,0 +1,153 @@
+#
+# SelfTest/Util/test_Padding.py: Self-test for padding functions
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 unittest
+from binascii import unhexlify as uh
+
+from Crypto.Util.py3compat import *
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Util.Padding import pad, unpad
+
+class PKCS7_Tests(unittest.TestCase):
+
+ def test1(self):
+ padded = pad(b(""), 4)
+ self.assertTrue(padded == uh(b("04040404")))
+ padded = pad(b(""), 4, 'pkcs7')
+ self.assertTrue(padded == uh(b("04040404")))
+ back = unpad(padded, 4)
+ self.assertTrue(back == b(""))
+
+ def test2(self):
+ padded = pad(uh(b("12345678")), 4)
+ self.assertTrue(padded == uh(b("1234567804040404")))
+ back = unpad(padded, 4)
+ self.assertTrue(back == uh(b("12345678")))
+
+ def test3(self):
+ padded = pad(uh(b("123456")), 4)
+ self.assertTrue(padded == uh(b("12345601")))
+ back = unpad(padded, 4)
+ self.assertTrue(back == uh(b("123456")))
+
+ def test4(self):
+ padded = pad(uh(b("1234567890")), 4)
+ self.assertTrue(padded == uh(b("1234567890030303")))
+ back = unpad(padded, 4)
+ self.assertTrue(back == uh(b("1234567890")))
+
+ def testn1(self):
+ self.assertRaises(ValueError, pad, uh(b("12")), 4, 'pkcs8')
+
+ def testn2(self):
+ self.assertRaises(ValueError, unpad, b("\0\0\0"), 4)
+ self.assertRaises(ValueError, unpad, b(""), 4)
+
+ def testn3(self):
+ self.assertRaises(ValueError, unpad, b("123456\x02"), 4)
+ self.assertRaises(ValueError, unpad, b("123456\x00"), 4)
+ self.assertRaises(ValueError, unpad, b("123456\x05\x05\x05\x05\x05"), 4)
+
+class X923_Tests(unittest.TestCase):
+
+ def test1(self):
+ padded = pad(b(""), 4, 'x923')
+ self.assertTrue(padded == uh(b("00000004")))
+ back = unpad(padded, 4, 'x923')
+ self.assertTrue(back == b(""))
+
+ def test2(self):
+ padded = pad(uh(b("12345678")), 4, 'x923')
+ self.assertTrue(padded == uh(b("1234567800000004")))
+ back = unpad(padded, 4, 'x923')
+ self.assertTrue(back == uh(b("12345678")))
+
+ def test3(self):
+ padded = pad(uh(b("123456")), 4, 'x923')
+ self.assertTrue(padded == uh(b("12345601")))
+ back = unpad(padded, 4, 'x923')
+ self.assertTrue(back == uh(b("123456")))
+
+ def test4(self):
+ padded = pad(uh(b("1234567890")), 4, 'x923')
+ self.assertTrue(padded == uh(b("1234567890000003")))
+ back = unpad(padded, 4, 'x923')
+ self.assertTrue(back == uh(b("1234567890")))
+
+ def testn1(self):
+ self.assertRaises(ValueError, unpad, b("123456\x02"), 4, 'x923')
+ self.assertRaises(ValueError, unpad, b("123456\x00"), 4, 'x923')
+ self.assertRaises(ValueError, unpad, b("123456\x00\x00\x00\x00\x05"), 4, 'x923')
+ self.assertRaises(ValueError, unpad, b(""), 4, 'x923')
+
+class ISO7816_Tests(unittest.TestCase):
+
+ def test1(self):
+ padded = pad(b(""), 4, 'iso7816')
+ self.assertTrue(padded == uh(b("80000000")))
+ back = unpad(padded, 4, 'iso7816')
+ self.assertTrue(back == b(""))
+
+ def test2(self):
+ padded = pad(uh(b("12345678")), 4, 'iso7816')
+ self.assertTrue(padded == uh(b("1234567880000000")))
+ back = unpad(padded, 4, 'iso7816')
+ self.assertTrue(back == uh(b("12345678")))
+
+ def test3(self):
+ padded = pad(uh(b("123456")), 4, 'iso7816')
+ self.assertTrue(padded == uh(b("12345680")))
+ back = unpad(padded, 4, 'iso7816')
+ self.assertTrue(back == uh(b("123456")))
+
+ def test4(self):
+ padded = pad(uh(b("1234567890")), 4, 'iso7816')
+ self.assertTrue(padded == uh(b("1234567890800000")))
+ back = unpad(padded, 4, 'iso7816')
+ self.assertTrue(back == uh(b("1234567890")))
+
+ def testn1(self):
+ self.assertRaises(ValueError, unpad, b("123456\x81"), 4, 'iso7816')
+ self.assertRaises(ValueError, unpad, b(""), 4, 'iso7816')
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(PKCS7_Tests)
+ tests += list_test_cases(X923_Tests)
+ tests += list_test_cases(ISO7816_Tests)
+ return tests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_asn1.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_asn1.py
new file mode 100644
index 00000000..e441ba0a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_asn1.py
@@ -0,0 +1,851 @@
+#
+# SelfTest/Util/test_asn.py: Self-test for the Crypto.Util.asn1 module
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""Self-tests for Crypto.Util.asn1"""
+
+import unittest
+
+from Crypto.Util.py3compat import *
+from Crypto.Util.asn1 import (DerObject, DerSetOf, DerInteger,
+ DerBitString,
+ DerObjectId, DerNull, DerOctetString,
+ DerSequence, DerBoolean)
+
+class DerObjectTests(unittest.TestCase):
+
+ def testObjInit1(self):
+ # Fail with invalid tag format (must be 1 byte)
+ self.assertRaises(ValueError, DerObject, b('\x00\x99'))
+ # Fail with invalid implicit tag (must be <0x1F)
+ self.assertRaises(ValueError, DerObject, 0x1F)
+
+ # ------
+
+ def testObjEncode1(self):
+ # No payload
+ der = DerObject(b('\x02'))
+ self.assertEqual(der.encode(), b('\x02\x00'))
+ # Small payload (primitive)
+ der.payload = b('\x45')
+ self.assertEqual(der.encode(), b('\x02\x01\x45'))
+ # Invariant
+ self.assertEqual(der.encode(), b('\x02\x01\x45'))
+ # Initialize with numerical tag
+ der = DerObject(0x04)
+ der.payload = b('\x45')
+ self.assertEqual(der.encode(), b('\x04\x01\x45'))
+ # Initialize with constructed type
+ der = DerObject(b('\x10'), constructed=True)
+ self.assertEqual(der.encode(), b('\x30\x00'))
+
+ def testObjEncode2(self):
+ # Initialize with payload
+ der = DerObject(0x03, b('\x12\x12'))
+ self.assertEqual(der.encode(), b('\x03\x02\x12\x12'))
+
+ def testObjEncode3(self):
+ # Long payload
+ der = DerObject(b('\x10'))
+ der.payload = b("0")*128
+ self.assertEqual(der.encode(), b('\x10\x81\x80' + "0"*128))
+
+ def testObjEncode4(self):
+ # Implicit tags (constructed)
+ der = DerObject(0x10, implicit=1, constructed=True)
+ der.payload = b('ppll')
+ self.assertEqual(der.encode(), b('\xa1\x04ppll'))
+ # Implicit tags (primitive)
+ der = DerObject(0x02, implicit=0x1E, constructed=False)
+ der.payload = b('ppll')
+ self.assertEqual(der.encode(), b('\x9E\x04ppll'))
+
+ def testObjEncode5(self):
+ # Encode type with explicit tag
+ der = DerObject(0x10, explicit=5)
+ der.payload = b("xxll")
+ self.assertEqual(der.encode(), b("\xa5\x06\x10\x04xxll"))
+
+ # -----
+
+ def testObjDecode1(self):
+ # Decode short payload
+ der = DerObject(0x02)
+ der.decode(b('\x02\x02\x01\x02'))
+ self.assertEqual(der.payload, b("\x01\x02"))
+ self.assertEqual(der._tag_octet, 0x02)
+
+ def testObjDecode2(self):
+ # Decode long payload
+ der = DerObject(0x02)
+ der.decode(b('\x02\x81\x80' + "1"*128))
+ self.assertEqual(der.payload, b("1")*128)
+ self.assertEqual(der._tag_octet, 0x02)
+
+ def testObjDecode3(self):
+ # Decode payload with too much data gives error
+ der = DerObject(0x02)
+ self.assertRaises(ValueError, der.decode, b('\x02\x02\x01\x02\xFF'))
+ # Decode payload with too little data gives error
+ der = DerObject(0x02)
+ self.assertRaises(ValueError, der.decode, b('\x02\x02\x01'))
+
+ def testObjDecode4(self):
+ # Decode implicit tag (primitive)
+ der = DerObject(0x02, constructed=False, implicit=0xF)
+ self.assertRaises(ValueError, der.decode, b('\x02\x02\x01\x02'))
+ der.decode(b('\x8F\x01\x00'))
+ self.assertEqual(der.payload, b('\x00'))
+ # Decode implicit tag (constructed)
+ der = DerObject(0x02, constructed=True, implicit=0xF)
+ self.assertRaises(ValueError, der.decode, b('\x02\x02\x01\x02'))
+ der.decode(b('\xAF\x01\x00'))
+ self.assertEqual(der.payload, b('\x00'))
+
+ def testObjDecode5(self):
+ # Decode payload with unexpected tag gives error
+ der = DerObject(0x02)
+ self.assertRaises(ValueError, der.decode, b('\x03\x02\x01\x02'))
+
+ def testObjDecode6(self):
+ # Arbitrary DER object
+ der = DerObject()
+ der.decode(b('\x65\x01\x88'))
+ self.assertEqual(der._tag_octet, 0x65)
+ self.assertEqual(der.payload, b('\x88'))
+
+ def testObjDecode7(self):
+ # Decode explicit tag
+ der = DerObject(0x10, explicit=5)
+ der.decode(b("\xa5\x06\x10\x04xxll"))
+ self.assertEqual(der._inner_tag_octet, 0x10)
+ self.assertEqual(der.payload, b('xxll'))
+
+ # Explicit tag may be 0
+ der = DerObject(0x10, explicit=0)
+ der.decode(b("\xa0\x06\x10\x04xxll"))
+ self.assertEqual(der._inner_tag_octet, 0x10)
+ self.assertEqual(der.payload, b('xxll'))
+
+ def testObjDecode8(self):
+ # Verify that decode returns the object
+ der = DerObject(0x02)
+ self.assertEqual(der, der.decode(b('\x02\x02\x01\x02')))
+
+class DerIntegerTests(unittest.TestCase):
+
+ def testInit1(self):
+ der = DerInteger(1)
+ self.assertEqual(der.encode(), b('\x02\x01\x01'))
+
+ def testEncode1(self):
+ # Single-byte integers
+ # Value 0
+ der = DerInteger(0)
+ self.assertEqual(der.encode(), b('\x02\x01\x00'))
+ # Value 1
+ der = DerInteger(1)
+ self.assertEqual(der.encode(), b('\x02\x01\x01'))
+ # Value 127
+ der = DerInteger(127)
+ self.assertEqual(der.encode(), b('\x02\x01\x7F'))
+
+ def testEncode2(self):
+ # Multi-byte integers
+ # Value 128
+ der = DerInteger(128)
+ self.assertEqual(der.encode(), b('\x02\x02\x00\x80'))
+ # Value 0x180
+ der = DerInteger(0x180)
+ self.assertEqual(der.encode(), b('\x02\x02\x01\x80'))
+ # One very long integer
+ der = DerInteger(2**2048)
+ self.assertEqual(der.encode(),
+ b('\x02\x82\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00'))
+
+ def testEncode3(self):
+ # Negative integers
+ # Value -1
+ der = DerInteger(-1)
+ self.assertEqual(der.encode(), b('\x02\x01\xFF'))
+ # Value -128
+ der = DerInteger(-128)
+ self.assertEqual(der.encode(), b('\x02\x01\x80'))
+ # Value
+ der = DerInteger(-87873)
+ self.assertEqual(der.encode(), b('\x02\x03\xFE\xA8\xBF'))
+
+ def testEncode4(self):
+ # Explicit encoding
+ number = DerInteger(0x34, explicit=3)
+ self.assertEqual(number.encode(), b('\xa3\x03\x02\x01\x34'))
+
+ # -----
+
+ def testDecode1(self):
+ # Single-byte integer
+ der = DerInteger()
+ # Value 0
+ der.decode(b('\x02\x01\x00'))
+ self.assertEqual(der.value, 0)
+ # Value 1
+ der.decode(b('\x02\x01\x01'))
+ self.assertEqual(der.value, 1)
+ # Value 127
+ der.decode(b('\x02\x01\x7F'))
+ self.assertEqual(der.value, 127)
+
+ def testDecode2(self):
+ # Multi-byte integer
+ der = DerInteger()
+ # Value 0x180L
+ der.decode(b('\x02\x02\x01\x80'))
+ self.assertEqual(der.value,0x180)
+ # One very long integer
+ der.decode(
+ b('\x02\x82\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00'))
+ self.assertEqual(der.value,2**2048)
+
+ def testDecode3(self):
+ # Negative integer
+ der = DerInteger()
+ # Value -1
+ der.decode(b('\x02\x01\xFF'))
+ self.assertEqual(der.value, -1)
+ # Value -32768
+ der.decode(b('\x02\x02\x80\x00'))
+ self.assertEqual(der.value, -32768)
+
+ def testDecode5(self):
+ # We still accept BER integer format
+ der = DerInteger()
+ # Redundant leading zeroes
+ der.decode(b('\x02\x02\x00\x01'))
+ self.assertEqual(der.value, 1)
+ # Redundant leading 0xFF
+ der.decode(b('\x02\x02\xFF\xFF'))
+ self.assertEqual(der.value, -1)
+ # Empty payload
+ der.decode(b('\x02\x00'))
+ self.assertEqual(der.value, 0)
+
+ def testDecode6(self):
+ # Explicit encoding
+ number = DerInteger(explicit=3)
+ number.decode(b('\xa3\x03\x02\x01\x34'))
+ self.assertEqual(number.value, 0x34)
+
+ def testDecode7(self):
+ # Verify decode returns the DerInteger
+ der = DerInteger()
+ self.assertEqual(der, der.decode(b('\x02\x01\x7F')))
+
+ ###
+
+ def testStrict1(self):
+ number = DerInteger()
+
+ number.decode(b'\x02\x02\x00\x01')
+ number.decode(b'\x02\x02\x00\x7F')
+ self.assertRaises(ValueError, number.decode, b'\x02\x02\x00\x01', strict=True)
+ self.assertRaises(ValueError, number.decode, b'\x02\x02\x00\x7F', strict=True)
+
+ ###
+
+ def testErrDecode1(self):
+ # Wide length field
+ der = DerInteger()
+ self.assertRaises(ValueError, der.decode, b('\x02\x81\x01\x01'))
+
+
+class DerSequenceTests(unittest.TestCase):
+
+ def testInit1(self):
+ der = DerSequence([1, DerInteger(2), b('0\x00')])
+ self.assertEqual(der.encode(), b('0\x08\x02\x01\x01\x02\x01\x020\x00'))
+
+ def testEncode1(self):
+ # Empty sequence
+ der = DerSequence()
+ self.assertEqual(der.encode(), b('0\x00'))
+ self.assertFalse(der.hasOnlyInts())
+ # One single-byte integer (zero)
+ der.append(0)
+ self.assertEqual(der.encode(), b('0\x03\x02\x01\x00'))
+ self.assertEqual(der.hasInts(),1)
+ self.assertEqual(der.hasInts(False),1)
+ self.assertTrue(der.hasOnlyInts())
+ self.assertTrue(der.hasOnlyInts(False))
+ # Invariant
+ self.assertEqual(der.encode(), b('0\x03\x02\x01\x00'))
+
+ def testEncode2(self):
+ # Indexing
+ der = DerSequence()
+ der.append(0)
+ der[0] = 1
+ self.assertEqual(len(der),1)
+ self.assertEqual(der[0],1)
+ self.assertEqual(der[-1],1)
+ self.assertEqual(der.encode(), b('0\x03\x02\x01\x01'))
+ #
+ der[:] = [1]
+ self.assertEqual(len(der),1)
+ self.assertEqual(der[0],1)
+ self.assertEqual(der.encode(), b('0\x03\x02\x01\x01'))
+
+ def testEncode3(self):
+ # One multi-byte integer (non-zero)
+ der = DerSequence()
+ der.append(0x180)
+ self.assertEqual(der.encode(), b('0\x04\x02\x02\x01\x80'))
+
+ def testEncode4(self):
+ # One very long integer
+ der = DerSequence()
+ der.append(2**2048)
+ self.assertEqual(der.encode(), b('0\x82\x01\x05')+
+ b('\x02\x82\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00'))
+
+ def testEncode5(self):
+ der = DerSequence()
+ der += 1
+ der += b('\x30\x00')
+ self.assertEqual(der.encode(), b('\x30\x05\x02\x01\x01\x30\x00'))
+
+ def testEncode6(self):
+ # Two positive integers
+ der = DerSequence()
+ der.append(0x180)
+ der.append(0xFF)
+ self.assertEqual(der.encode(), b('0\x08\x02\x02\x01\x80\x02\x02\x00\xff'))
+ self.assertTrue(der.hasOnlyInts())
+ self.assertTrue(der.hasOnlyInts(False))
+ # Two mixed integers
+ der = DerSequence()
+ der.append(2)
+ der.append(-2)
+ self.assertEqual(der.encode(), b('0\x06\x02\x01\x02\x02\x01\xFE'))
+ self.assertEqual(der.hasInts(), 1)
+ self.assertEqual(der.hasInts(False), 2)
+ self.assertFalse(der.hasOnlyInts())
+ self.assertTrue(der.hasOnlyInts(False))
+ #
+ der.append(0x01)
+ der[1:] = [9,8]
+ self.assertEqual(len(der),3)
+ self.assertEqual(der[1:],[9,8])
+ self.assertEqual(der[1:-1],[9])
+ self.assertEqual(der.encode(), b('0\x09\x02\x01\x02\x02\x01\x09\x02\x01\x08'))
+
+ def testEncode7(self):
+ # One integer and another type (already encoded)
+ der = DerSequence()
+ der.append(0x180)
+ der.append(b('0\x03\x02\x01\x05'))
+ self.assertEqual(der.encode(), b('0\x09\x02\x02\x01\x800\x03\x02\x01\x05'))
+ self.assertFalse(der.hasOnlyInts())
+
+ def testEncode8(self):
+ # One integer and another type (yet to encode)
+ der = DerSequence()
+ der.append(0x180)
+ der.append(DerSequence([5]))
+ self.assertEqual(der.encode(), b('0\x09\x02\x02\x01\x800\x03\x02\x01\x05'))
+ self.assertFalse(der.hasOnlyInts())
+
+ ####
+
+ def testDecode1(self):
+ # Empty sequence
+ der = DerSequence()
+ der.decode(b('0\x00'))
+ self.assertEqual(len(der),0)
+ # One single-byte integer (zero)
+ der.decode(b('0\x03\x02\x01\x00'))
+ self.assertEqual(len(der),1)
+ self.assertEqual(der[0],0)
+ # Invariant
+ der.decode(b('0\x03\x02\x01\x00'))
+ self.assertEqual(len(der),1)
+ self.assertEqual(der[0],0)
+
+ def testDecode2(self):
+ # One single-byte integer (non-zero)
+ der = DerSequence()
+ der.decode(b('0\x03\x02\x01\x7f'))
+ self.assertEqual(len(der),1)
+ self.assertEqual(der[0],127)
+
+ def testDecode4(self):
+ # One very long integer
+ der = DerSequence()
+ der.decode(b('0\x82\x01\x05')+
+ b('\x02\x82\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')+
+ b('\x00\x00\x00\x00\x00\x00\x00\x00\x00'))
+ self.assertEqual(len(der),1)
+ self.assertEqual(der[0],2**2048)
+
+ def testDecode6(self):
+ # Two integers
+ der = DerSequence()
+ der.decode(b('0\x08\x02\x02\x01\x80\x02\x02\x00\xff'))
+ self.assertEqual(len(der),2)
+ self.assertEqual(der[0],0x180)
+ self.assertEqual(der[1],0xFF)
+
+ def testDecode7(self):
+ # One integer and 2 other types
+ der = DerSequence()
+ der.decode(b('0\x0A\x02\x02\x01\x80\x24\x02\xb6\x63\x12\x00'))
+ self.assertEqual(len(der),3)
+ self.assertEqual(der[0],0x180)
+ self.assertEqual(der[1],b('\x24\x02\xb6\x63'))
+ self.assertEqual(der[2],b('\x12\x00'))
+
+ def testDecode8(self):
+ # Only 2 other types
+ der = DerSequence()
+ der.decode(b('0\x06\x24\x02\xb6\x63\x12\x00'))
+ self.assertEqual(len(der),2)
+ self.assertEqual(der[0],b('\x24\x02\xb6\x63'))
+ self.assertEqual(der[1],b('\x12\x00'))
+ self.assertEqual(der.hasInts(), 0)
+ self.assertEqual(der.hasInts(False), 0)
+ self.assertFalse(der.hasOnlyInts())
+ self.assertFalse(der.hasOnlyInts(False))
+
+ def testDecode9(self):
+ # Verify that decode returns itself
+ der = DerSequence()
+ self.assertEqual(der, der.decode(b('0\x06\x24\x02\xb6\x63\x12\x00')))
+
+ ###
+
+ def testErrDecode1(self):
+ # Not a sequence
+ der = DerSequence()
+ self.assertRaises(ValueError, der.decode, b(''))
+ self.assertRaises(ValueError, der.decode, b('\x00'))
+ self.assertRaises(ValueError, der.decode, b('\x30'))
+
+ def testErrDecode2(self):
+ der = DerSequence()
+ # Too much data
+ self.assertRaises(ValueError, der.decode, b('\x30\x00\x00'))
+
+ def testErrDecode3(self):
+ # Wrong length format
+ der = DerSequence()
+ # Missing length in sub-item
+ self.assertRaises(ValueError, der.decode, b('\x30\x04\x02\x01\x01\x00'))
+ # Valid BER, but invalid DER length
+ self.assertRaises(ValueError, der.decode, b('\x30\x81\x03\x02\x01\x01'))
+ self.assertRaises(ValueError, der.decode, b('\x30\x04\x02\x81\x01\x01'))
+
+ def test_expected_nr_elements(self):
+ der_bin = DerSequence([1, 2, 3]).encode()
+
+ DerSequence().decode(der_bin, nr_elements=3)
+ DerSequence().decode(der_bin, nr_elements=(2,3))
+ self.assertRaises(ValueError, DerSequence().decode, der_bin, nr_elements=1)
+ self.assertRaises(ValueError, DerSequence().decode, der_bin, nr_elements=(4,5))
+
+ def test_expected_only_integers(self):
+
+ der_bin1 = DerSequence([1, 2, 3]).encode()
+ der_bin2 = DerSequence([1, 2, DerSequence([3, 4])]).encode()
+
+ DerSequence().decode(der_bin1, only_ints_expected=True)
+ DerSequence().decode(der_bin1, only_ints_expected=False)
+ DerSequence().decode(der_bin2, only_ints_expected=False)
+ self.assertRaises(ValueError, DerSequence().decode, der_bin2, only_ints_expected=True)
+
+
+class DerOctetStringTests(unittest.TestCase):
+
+ def testInit1(self):
+ der = DerOctetString(b('\xFF'))
+ self.assertEqual(der.encode(), b('\x04\x01\xFF'))
+
+ def testEncode1(self):
+ # Empty sequence
+ der = DerOctetString()
+ self.assertEqual(der.encode(), b('\x04\x00'))
+ # Small payload
+ der.payload = b('\x01\x02')
+ self.assertEqual(der.encode(), b('\x04\x02\x01\x02'))
+
+ ####
+
+ def testDecode1(self):
+ # Empty sequence
+ der = DerOctetString()
+ der.decode(b('\x04\x00'))
+ self.assertEqual(der.payload, b(''))
+ # Small payload
+ der.decode(b('\x04\x02\x01\x02'))
+ self.assertEqual(der.payload, b('\x01\x02'))
+
+ def testDecode2(self):
+ # Verify that decode returns the object
+ der = DerOctetString()
+ self.assertEqual(der, der.decode(b('\x04\x00')))
+
+ def testErrDecode1(self):
+ # No leftovers allowed
+ der = DerOctetString()
+ self.assertRaises(ValueError, der.decode, b('\x04\x01\x01\xff'))
+
+class DerNullTests(unittest.TestCase):
+
+ def testEncode1(self):
+ der = DerNull()
+ self.assertEqual(der.encode(), b('\x05\x00'))
+
+ ####
+
+ def testDecode1(self):
+ # Empty sequence
+ der = DerNull()
+ self.assertEqual(der, der.decode(b('\x05\x00')))
+
+class DerObjectIdTests(unittest.TestCase):
+
+ def testInit1(self):
+ der = DerObjectId("1.1")
+ self.assertEqual(der.encode(), b'\x06\x01)')
+
+ def testEncode1(self):
+ der = DerObjectId('1.2.840.113549.1.1.1')
+ self.assertEqual(der.encode(), b'\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01')
+
+ der = DerObjectId()
+ der.value = '1.2.840.113549.1.1.1'
+ self.assertEqual(der.encode(), b'\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01')
+
+ der = DerObjectId('2.999.1234')
+ self.assertEqual(der.encode(), b'\x06\x04\x88\x37\x89\x52')
+
+ def testEncode2(self):
+ der = DerObjectId('3.4')
+ self.assertRaises(ValueError, der.encode)
+
+ der = DerObjectId('1.40')
+ self.assertRaises(ValueError, der.encode)
+
+ ####
+
+ def testDecode1(self):
+ # Empty sequence
+ der = DerObjectId()
+ der.decode(b'\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01')
+ self.assertEqual(der.value, '1.2.840.113549.1.1.1')
+
+ def testDecode2(self):
+ # Verify that decode returns the object
+ der = DerObjectId()
+ self.assertEqual(der,
+ der.decode(b'\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01'))
+
+ def testDecode3(self):
+ der = DerObjectId()
+ der.decode(b'\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x00\x01')
+ self.assertEqual(der.value, '1.2.840.113549.1.0.1')
+
+ def testDecode4(self):
+ der = DerObjectId()
+ der.decode(b'\x06\x04\x88\x37\x89\x52')
+ self.assertEqual(der.value, '2.999.1234')
+
+
+class DerBitStringTests(unittest.TestCase):
+
+ def testInit1(self):
+ der = DerBitString(b("\xFF"))
+ self.assertEqual(der.encode(), b('\x03\x02\x00\xFF'))
+
+ def testInit2(self):
+ der = DerBitString(DerInteger(1))
+ self.assertEqual(der.encode(), b('\x03\x04\x00\x02\x01\x01'))
+
+ def testEncode1(self):
+ # Empty sequence
+ der = DerBitString()
+ self.assertEqual(der.encode(), b('\x03\x01\x00'))
+ # Small payload
+ der = DerBitString(b('\x01\x02'))
+ self.assertEqual(der.encode(), b('\x03\x03\x00\x01\x02'))
+ # Small payload
+ der = DerBitString()
+ der.value = b('\x01\x02')
+ self.assertEqual(der.encode(), b('\x03\x03\x00\x01\x02'))
+
+ ####
+
+ def testDecode1(self):
+ # Empty sequence
+ der = DerBitString()
+ der.decode(b('\x03\x00'))
+ self.assertEqual(der.value, b(''))
+ # Small payload
+ der.decode(b('\x03\x03\x00\x01\x02'))
+ self.assertEqual(der.value, b('\x01\x02'))
+
+ def testDecode2(self):
+ # Verify that decode returns the object
+ der = DerBitString()
+ self.assertEqual(der, der.decode(b('\x03\x00')))
+
+
+class DerSetOfTests(unittest.TestCase):
+
+ def testInit1(self):
+ der = DerSetOf([DerInteger(1), DerInteger(2)])
+ self.assertEqual(der.encode(), b('1\x06\x02\x01\x01\x02\x01\x02'))
+
+ def testEncode1(self):
+ # Empty set
+ der = DerSetOf()
+ self.assertEqual(der.encode(), b('1\x00'))
+ # One single-byte integer (zero)
+ der.add(0)
+ self.assertEqual(der.encode(), b('1\x03\x02\x01\x00'))
+ # Invariant
+ self.assertEqual(der.encode(), b('1\x03\x02\x01\x00'))
+
+ def testEncode2(self):
+ # Two integers
+ der = DerSetOf()
+ der.add(0x180)
+ der.add(0xFF)
+ self.assertEqual(der.encode(), b('1\x08\x02\x02\x00\xff\x02\x02\x01\x80'))
+ # Initialize with integers
+ der = DerSetOf([0x180, 0xFF])
+ self.assertEqual(der.encode(), b('1\x08\x02\x02\x00\xff\x02\x02\x01\x80'))
+
+ def testEncode3(self):
+ # One integer and another type (no matter what it is)
+ der = DerSetOf()
+ der.add(0x180)
+ self.assertRaises(ValueError, der.add, b('\x00\x02\x00\x00'))
+
+ def testEncode4(self):
+ # Only non integers
+ der = DerSetOf()
+ der.add(b('\x01\x00'))
+ der.add(b('\x01\x01\x01'))
+ self.assertEqual(der.encode(), b('1\x05\x01\x00\x01\x01\x01'))
+
+ ####
+
+ def testDecode1(self):
+ # Empty sequence
+ der = DerSetOf()
+ der.decode(b('1\x00'))
+ self.assertEqual(len(der),0)
+ # One single-byte integer (zero)
+ der.decode(b('1\x03\x02\x01\x00'))
+ self.assertEqual(len(der),1)
+ self.assertEqual(list(der),[0])
+
+ def testDecode2(self):
+ # Two integers
+ der = DerSetOf()
+ der.decode(b('1\x08\x02\x02\x01\x80\x02\x02\x00\xff'))
+ self.assertEqual(len(der),2)
+ l = list(der)
+ self.assertTrue(0x180 in l)
+ self.assertTrue(0xFF in l)
+
+ def testDecode3(self):
+ # One integer and 2 other types
+ der = DerSetOf()
+ #import pdb; pdb.set_trace()
+ self.assertRaises(ValueError, der.decode,
+ b('0\x0A\x02\x02\x01\x80\x24\x02\xb6\x63\x12\x00'))
+
+ def testDecode4(self):
+ # Verify that decode returns the object
+ der = DerSetOf()
+ self.assertEqual(der,
+ der.decode(b('1\x08\x02\x02\x01\x80\x02\x02\x00\xff')))
+
+ ###
+
+ def testErrDecode1(self):
+ # No leftovers allowed
+ der = DerSetOf()
+ self.assertRaises(ValueError, der.decode,
+ b('1\x08\x02\x02\x01\x80\x02\x02\x00\xff\xAA'))
+
+
+class DerBooleanTests(unittest.TestCase):
+
+ def testEncode1(self):
+ der = DerBoolean(False)
+ self.assertEqual(der.encode(), b'\x01\x01\x00')
+
+ def testEncode2(self):
+ der = DerBoolean(True)
+ self.assertEqual(der.encode(), b'\x01\x01\xFF')
+
+ def testEncode3(self):
+ der = DerBoolean(False, implicit=0x12)
+ self.assertEqual(der.encode(), b'\x92\x01\x00')
+
+ def testEncode4(self):
+ der = DerBoolean(False, explicit=0x05)
+ self.assertEqual(der.encode(), b'\xA5\x03\x01\x01\x00')
+ ####
+
+ def testDecode1(self):
+ der = DerBoolean()
+ der.decode(b'\x01\x01\x00')
+ self.assertEqual(der.value, False)
+
+ def testDecode2(self):
+ der = DerBoolean()
+ der.decode(b'\x01\x01\xFF')
+ self.assertEqual(der.value, True)
+
+ def testDecode3(self):
+ der = DerBoolean(implicit=0x12)
+ der.decode(b'\x92\x01\x00')
+ self.assertEqual(der.value, False)
+
+ def testDecode4(self):
+ der = DerBoolean(explicit=0x05)
+ der.decode(b'\xA5\x03\x01\x01\x00')
+ self.assertEqual(der.value, False)
+
+ def testErrorDecode1(self):
+ der = DerBoolean()
+ # Wrong tag
+ self.assertRaises(ValueError, der.decode, b'\x02\x01\x00')
+
+ def testErrorDecode2(self):
+ der = DerBoolean()
+ # Payload too long
+ self.assertRaises(ValueError, der.decode, b'\x01\x01\x00\xFF')
+
+
+def get_tests(config={}):
+ from Crypto.SelfTest.st_common import list_test_cases
+ listTests = []
+ listTests += list_test_cases(DerObjectTests)
+ listTests += list_test_cases(DerIntegerTests)
+ listTests += list_test_cases(DerSequenceTests)
+ listTests += list_test_cases(DerOctetStringTests)
+ listTests += list_test_cases(DerNullTests)
+ listTests += list_test_cases(DerObjectIdTests)
+ listTests += list_test_cases(DerBitStringTests)
+ listTests += list_test_cases(DerSetOfTests)
+ listTests += list_test_cases(DerBooleanTests)
+ return listTests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_number.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_number.py
new file mode 100644
index 00000000..bb143f3b
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_number.py
@@ -0,0 +1,192 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/Util/test_number.py: Self-test for parts of the Crypto.Util.number module
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self-tests for (some of) Crypto.Util.number"""
+
+import math
+import unittest
+
+from Crypto.Util.py3compat import *
+from Crypto.SelfTest.st_common import list_test_cases
+
+from Crypto.Util import number
+from Crypto.Util.number import long_to_bytes
+
+
+class MyError(Exception):
+ """Dummy exception used for tests"""
+
+# NB: In some places, we compare tuples instead of just output values so that
+# if any inputs cause a test failure, we'll be able to tell which ones.
+
+class MiscTests(unittest.TestCase):
+
+ def test_ceil_div(self):
+ """Util.number.ceil_div"""
+ self.assertRaises(TypeError, number.ceil_div, "1", 1)
+ self.assertRaises(ZeroDivisionError, number.ceil_div, 1, 0)
+ self.assertRaises(ZeroDivisionError, number.ceil_div, -1, 0)
+
+ # b = 1
+ self.assertEqual(0, number.ceil_div(0, 1))
+ self.assertEqual(1, number.ceil_div(1, 1))
+ self.assertEqual(2, number.ceil_div(2, 1))
+ self.assertEqual(3, number.ceil_div(3, 1))
+
+ # b = 2
+ self.assertEqual(0, number.ceil_div(0, 2))
+ self.assertEqual(1, number.ceil_div(1, 2))
+ self.assertEqual(1, number.ceil_div(2, 2))
+ self.assertEqual(2, number.ceil_div(3, 2))
+ self.assertEqual(2, number.ceil_div(4, 2))
+ self.assertEqual(3, number.ceil_div(5, 2))
+
+ # b = 3
+ self.assertEqual(0, number.ceil_div(0, 3))
+ self.assertEqual(1, number.ceil_div(1, 3))
+ self.assertEqual(1, number.ceil_div(2, 3))
+ self.assertEqual(1, number.ceil_div(3, 3))
+ self.assertEqual(2, number.ceil_div(4, 3))
+ self.assertEqual(2, number.ceil_div(5, 3))
+ self.assertEqual(2, number.ceil_div(6, 3))
+ self.assertEqual(3, number.ceil_div(7, 3))
+
+ # b = 4
+ self.assertEqual(0, number.ceil_div(0, 4))
+ self.assertEqual(1, number.ceil_div(1, 4))
+ self.assertEqual(1, number.ceil_div(2, 4))
+ self.assertEqual(1, number.ceil_div(3, 4))
+ self.assertEqual(1, number.ceil_div(4, 4))
+ self.assertEqual(2, number.ceil_div(5, 4))
+ self.assertEqual(2, number.ceil_div(6, 4))
+ self.assertEqual(2, number.ceil_div(7, 4))
+ self.assertEqual(2, number.ceil_div(8, 4))
+ self.assertEqual(3, number.ceil_div(9, 4))
+
+ def test_getPrime(self):
+ """Util.number.getPrime"""
+ self.assertRaises(ValueError, number.getPrime, -100)
+ self.assertRaises(ValueError, number.getPrime, 0)
+ self.assertRaises(ValueError, number.getPrime, 1)
+
+ bits = 4
+ for i in range(100):
+ x = number.getPrime(bits)
+ self.assertEqual(x >= (1 << bits - 1), 1)
+ self.assertEqual(x < (1 << bits), 1)
+
+ bits = 512
+ x = number.getPrime(bits)
+ self.assertNotEqual(x % 2, 0)
+ self.assertEqual(x >= (1 << bits - 1), 1)
+ self.assertEqual(x < (1 << bits), 1)
+
+ def test_getStrongPrime(self):
+ """Util.number.getStrongPrime"""
+ self.assertRaises(ValueError, number.getStrongPrime, 256)
+ self.assertRaises(ValueError, number.getStrongPrime, 513)
+ bits = 512
+ x = number.getStrongPrime(bits)
+ self.assertNotEqual(x % 2, 0)
+ self.assertEqual(x > (1 << bits-1)-1, 1)
+ self.assertEqual(x < (1 << bits), 1)
+ e = 2**16+1
+ x = number.getStrongPrime(bits, e)
+ self.assertEqual(number.GCD(x-1, e), 1)
+ self.assertNotEqual(x % 2, 0)
+ self.assertEqual(x > (1 << bits-1)-1, 1)
+ self.assertEqual(x < (1 << bits), 1)
+ e = 2**16+2
+ x = number.getStrongPrime(bits, e)
+ self.assertEqual(number.GCD((x-1)>>1, e), 1)
+ self.assertNotEqual(x % 2, 0)
+ self.assertEqual(x > (1 << bits-1)-1, 1)
+ self.assertEqual(x < (1 << bits), 1)
+
+ def test_isPrime(self):
+ """Util.number.isPrime"""
+ self.assertEqual(number.isPrime(-3), False) # Regression test: negative numbers should not be prime
+ self.assertEqual(number.isPrime(-2), False) # Regression test: negative numbers should not be prime
+ self.assertEqual(number.isPrime(1), False) # Regression test: isPrime(1) caused some versions of PyCrypto to crash.
+ self.assertEqual(number.isPrime(2), True)
+ self.assertEqual(number.isPrime(3), True)
+ self.assertEqual(number.isPrime(4), False)
+ self.assertEqual(number.isPrime(2**1279-1), True)
+ self.assertEqual(number.isPrime(-(2**1279-1)), False) # Regression test: negative numbers should not be prime
+ # test some known gmp pseudo-primes taken from
+ # http://www.trnicely.net/misc/mpzspsp.html
+ for composite in (43 * 127 * 211, 61 * 151 * 211, 15259 * 30517,
+ 346141 * 692281, 1007119 * 2014237, 3589477 * 7178953,
+ 4859419 * 9718837, 2730439 * 5460877,
+ 245127919 * 490255837, 963939391 * 1927878781,
+ 4186358431 * 8372716861, 1576820467 * 3153640933):
+ self.assertEqual(number.isPrime(int(composite)), False)
+
+ def test_size(self):
+ self.assertEqual(number.size(2),2)
+ self.assertEqual(number.size(3),2)
+ self.assertEqual(number.size(0xa2),8)
+ self.assertEqual(number.size(0xa2ba40),8*3)
+ self.assertEqual(number.size(0xa2ba40ee07e3b2bd2f02ce227f36a195024486e49c19cb41bbbdfbba98b22b0e577c2eeaffa20d883a76e65e394c69d4b3c05a1e8fadda27edb2a42bc000fe888b9b32c22d15add0cd76b3e7936e19955b220dd17d4ea904b1ec102b2e4de7751222aa99151024c7cb41cc5ea21d00eeb41f7c800834d2c6e06bce3bce7ea9a5), 1024)
+ self.assertRaises(ValueError, number.size, -1)
+
+
+class LongTests(unittest.TestCase):
+
+ def test1(self):
+ self.assertEqual(long_to_bytes(0), b'\x00')
+ self.assertEqual(long_to_bytes(1), b'\x01')
+ self.assertEqual(long_to_bytes(0x100), b'\x01\x00')
+ self.assertEqual(long_to_bytes(0xFF00000000), b'\xFF\x00\x00\x00\x00')
+ self.assertEqual(long_to_bytes(0xFF00000000), b'\xFF\x00\x00\x00\x00')
+ self.assertEqual(long_to_bytes(0x1122334455667788), b'\x11\x22\x33\x44\x55\x66\x77\x88')
+ self.assertEqual(long_to_bytes(0x112233445566778899), b'\x11\x22\x33\x44\x55\x66\x77\x88\x99')
+
+ def test2(self):
+ self.assertEqual(long_to_bytes(0, 1), b'\x00')
+ self.assertEqual(long_to_bytes(0, 2), b'\x00\x00')
+ self.assertEqual(long_to_bytes(1, 3), b'\x00\x00\x01')
+ self.assertEqual(long_to_bytes(65535, 2), b'\xFF\xFF')
+ self.assertEqual(long_to_bytes(65536, 2), b'\x00\x01\x00\x00')
+ self.assertEqual(long_to_bytes(0x100, 1), b'\x01\x00')
+ self.assertEqual(long_to_bytes(0xFF00000001, 6), b'\x00\xFF\x00\x00\x00\x01')
+ self.assertEqual(long_to_bytes(0xFF00000001, 8), b'\x00\x00\x00\xFF\x00\x00\x00\x01')
+ self.assertEqual(long_to_bytes(0xFF00000001, 10), b'\x00\x00\x00\x00\x00\xFF\x00\x00\x00\x01')
+ self.assertEqual(long_to_bytes(0xFF00000001, 11), b'\x00\x00\x00\x00\x00\x00\xFF\x00\x00\x00\x01')
+
+ def test_err1(self):
+ self.assertRaises(ValueError, long_to_bytes, -1)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(MiscTests)
+ tests += list_test_cases(LongTests)
+ return tests
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_rfc1751.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_rfc1751.py
new file mode 100644
index 00000000..af0aa2ba
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_rfc1751.py
@@ -0,0 +1,38 @@
+import unittest
+
+import binascii
+from Crypto.Util.RFC1751 import key_to_english, english_to_key
+
+
+class RFC1751_Tests(unittest.TestCase):
+
+ def test1(self):
+ data = [
+ ('EB33F77EE73D4053', 'TIDE ITCH SLOW REIN RULE MOT'),
+ ('CCAC2AED591056BE4F90FD441C534766', 'RASH BUSH MILK LOOK BAD BRIM AVID GAFF BAIT ROT POD LOVE'),
+ ('EFF81F9BFBC65350920CDD7416DE8009', 'TROD MUTE TAIL WARM CHAR KONG HAAG CITY BORE O TEAL AWL')
+ ]
+
+ for key_hex, words in data:
+ key_bin = binascii.a2b_hex(key_hex)
+
+ w2 = key_to_english(key_bin)
+ self.assertEqual(w2, words)
+
+ k2 = english_to_key(words)
+ self.assertEqual(k2, key_bin)
+
+ def test_error_key_to_english(self):
+
+ self.assertRaises(ValueError, key_to_english, b'0' * 7)
+
+
+def get_tests(config={}):
+ from Crypto.SelfTest.st_common import list_test_cases
+ tests = list_test_cases(RFC1751_Tests)
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_strxor.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_strxor.py
new file mode 100644
index 00000000..c91d38f5
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/Util/test_strxor.py
@@ -0,0 +1,280 @@
+#
+# SelfTest/Util/test_strxor.py: Self-test for XORing
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 unittest
+from binascii import unhexlify, hexlify
+
+from Crypto.SelfTest.st_common import list_test_cases
+from Crypto.Util.strxor import strxor, strxor_c
+
+
+class StrxorTests(unittest.TestCase):
+
+ def test1(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ term2 = unhexlify(b"383d4ba020573314395b")
+ result = unhexlify(b"c70ed123c59a7fcb6f12")
+ self.assertEqual(strxor(term1, term2), result)
+ self.assertEqual(strxor(term2, term1), result)
+
+ def test2(self):
+ es = b""
+ self.assertEqual(strxor(es, es), es)
+
+ def test3(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ all_zeros = b"\x00" * len(term1)
+ self.assertEqual(strxor(term1, term1), all_zeros)
+
+ def test_wrong_length(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ term2 = unhexlify(b"ff339a83e5cd4cdf564990")
+ self.assertRaises(ValueError, strxor, term1, term2)
+
+ def test_bytearray(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ term1_ba = bytearray(term1)
+ term2 = unhexlify(b"383d4ba020573314395b")
+ result = unhexlify(b"c70ed123c59a7fcb6f12")
+
+ self.assertEqual(strxor(term1_ba, term2), result)
+
+ def test_memoryview(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ term1_mv = memoryview(term1)
+ term2 = unhexlify(b"383d4ba020573314395b")
+ result = unhexlify(b"c70ed123c59a7fcb6f12")
+
+ self.assertEqual(strxor(term1_mv, term2), result)
+
+ def test_output_bytearray(self):
+ """Verify result can be stored in pre-allocated memory"""
+
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ term2 = unhexlify(b"383d4ba020573314395b")
+ original_term1 = term1[:]
+ original_term2 = term2[:]
+ expected_xor = unhexlify(b"c70ed123c59a7fcb6f12")
+ output = bytearray(len(term1))
+
+ result = strxor(term1, term2, output=output)
+
+ self.assertEqual(result, None)
+ self.assertEqual(output, expected_xor)
+ self.assertEqual(term1, original_term1)
+ self.assertEqual(term2, original_term2)
+
+ def test_output_memoryview(self):
+ """Verify result can be stored in pre-allocated memory"""
+
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ term2 = unhexlify(b"383d4ba020573314395b")
+ original_term1 = term1[:]
+ original_term2 = term2[:]
+ expected_xor = unhexlify(b"c70ed123c59a7fcb6f12")
+ output = memoryview(bytearray(len(term1)))
+
+ result = strxor(term1, term2, output=output)
+
+ self.assertEqual(result, None)
+ self.assertEqual(output, expected_xor)
+ self.assertEqual(term1, original_term1)
+ self.assertEqual(term2, original_term2)
+
+ def test_output_overlapping_bytearray(self):
+ """Verify result can be stored in overlapping memory"""
+
+ term1 = bytearray(unhexlify(b"ff339a83e5cd4cdf5649"))
+ term2 = unhexlify(b"383d4ba020573314395b")
+ original_term2 = term2[:]
+ expected_xor = unhexlify(b"c70ed123c59a7fcb6f12")
+
+ result = strxor(term1, term2, output=term1)
+
+ self.assertEqual(result, None)
+ self.assertEqual(term1, expected_xor)
+ self.assertEqual(term2, original_term2)
+
+ def test_output_overlapping_memoryview(self):
+ """Verify result can be stored in overlapping memory"""
+
+ term1 = memoryview(bytearray(unhexlify(b"ff339a83e5cd4cdf5649")))
+ term2 = unhexlify(b"383d4ba020573314395b")
+ original_term2 = term2[:]
+ expected_xor = unhexlify(b"c70ed123c59a7fcb6f12")
+
+ result = strxor(term1, term2, output=term1)
+
+ self.assertEqual(result, None)
+ self.assertEqual(term1, expected_xor)
+ self.assertEqual(term2, original_term2)
+
+ def test_output_ro_bytes(self):
+ """Verify result cannot be stored in read-only memory"""
+
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ term2 = unhexlify(b"383d4ba020573314395b")
+
+ self.assertRaises(TypeError, strxor, term1, term2, output=term1)
+
+ def test_output_ro_memoryview(self):
+ """Verify result cannot be stored in read-only memory"""
+
+ term1 = memoryview(unhexlify(b"ff339a83e5cd4cdf5649"))
+ term2 = unhexlify(b"383d4ba020573314395b")
+
+ self.assertRaises(TypeError, strxor, term1, term2, output=term1)
+
+ def test_output_incorrect_length(self):
+ """Verify result cannot be stored in memory of incorrect length"""
+
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ term2 = unhexlify(b"383d4ba020573314395b")
+ output = bytearray(len(term1) - 1)
+
+ self.assertRaises(ValueError, strxor, term1, term2, output=output)
+
+
+class Strxor_cTests(unittest.TestCase):
+
+ def test1(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ result = unhexlify(b"be72dbc2a48c0d9e1708")
+ self.assertEqual(strxor_c(term1, 65), result)
+
+ def test2(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ self.assertEqual(strxor_c(term1, 0), term1)
+
+ def test3(self):
+ self.assertEqual(strxor_c(b"", 90), b"")
+
+ def test_wrong_range(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ self.assertRaises(ValueError, strxor_c, term1, -1)
+ self.assertRaises(ValueError, strxor_c, term1, 256)
+
+ def test_bytearray(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ term1_ba = bytearray(term1)
+ result = unhexlify(b"be72dbc2a48c0d9e1708")
+
+ self.assertEqual(strxor_c(term1_ba, 65), result)
+
+ def test_memoryview(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ term1_mv = memoryview(term1)
+ result = unhexlify(b"be72dbc2a48c0d9e1708")
+
+ self.assertEqual(strxor_c(term1_mv, 65), result)
+
+ def test_output_bytearray(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ original_term1 = term1[:]
+ expected_result = unhexlify(b"be72dbc2a48c0d9e1708")
+ output = bytearray(len(term1))
+
+ result = strxor_c(term1, 65, output=output)
+
+ self.assertEqual(result, None)
+ self.assertEqual(output, expected_result)
+ self.assertEqual(term1, original_term1)
+
+ def test_output_memoryview(self):
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ original_term1 = term1[:]
+ expected_result = unhexlify(b"be72dbc2a48c0d9e1708")
+ output = memoryview(bytearray(len(term1)))
+
+ result = strxor_c(term1, 65, output=output)
+
+ self.assertEqual(result, None)
+ self.assertEqual(output, expected_result)
+ self.assertEqual(term1, original_term1)
+
+ def test_output_overlapping_bytearray(self):
+ """Verify result can be stored in overlapping memory"""
+
+ term1 = bytearray(unhexlify(b"ff339a83e5cd4cdf5649"))
+ expected_xor = unhexlify(b"be72dbc2a48c0d9e1708")
+
+ result = strxor_c(term1, 65, output=term1)
+
+ self.assertEqual(result, None)
+ self.assertEqual(term1, expected_xor)
+
+ def test_output_overlapping_memoryview(self):
+ """Verify result can be stored in overlapping memory"""
+
+ term1 = memoryview(bytearray(unhexlify(b"ff339a83e5cd4cdf5649")))
+ expected_xor = unhexlify(b"be72dbc2a48c0d9e1708")
+
+ result = strxor_c(term1, 65, output=term1)
+
+ self.assertEqual(result, None)
+ self.assertEqual(term1, expected_xor)
+
+ def test_output_ro_bytes(self):
+ """Verify result cannot be stored in read-only memory"""
+
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+
+ self.assertRaises(TypeError, strxor_c, term1, 65, output=term1)
+
+ def test_output_ro_memoryview(self):
+ """Verify result cannot be stored in read-only memory"""
+
+ term1 = memoryview(unhexlify(b"ff339a83e5cd4cdf5649"))
+ term2 = unhexlify(b"383d4ba020573314395b")
+
+ self.assertRaises(TypeError, strxor_c, term1, 65, output=term1)
+
+ def test_output_incorrect_length(self):
+ """Verify result cannot be stored in memory of incorrect length"""
+
+ term1 = unhexlify(b"ff339a83e5cd4cdf5649")
+ output = bytearray(len(term1) - 1)
+
+ self.assertRaises(ValueError, strxor_c, term1, 65, output=output)
+
+
+def get_tests(config={}):
+ tests = []
+ tests += list_test_cases(StrxorTests)
+ tests += list_test_cases(Strxor_cTests)
+ return tests
+
+
+if __name__ == '__main__':
+ suite = lambda: unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/__init__.py
new file mode 100644
index 00000000..3a4b8b62
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/__init__.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/__init__.py: Self-test for PyCrypto
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Self tests
+
+These tests should perform quickly and can ideally be used every time an
+application runs.
+"""
+
+import sys
+import unittest
+from importlib import import_module
+from Crypto.Util.py3compat import StringIO
+
+
+class SelfTestError(Exception):
+ def __init__(self, message, result):
+ Exception.__init__(self, message, result)
+ self.message = message
+ self.result = result
+
+
+def run(module=None, verbosity=0, stream=None, tests=None, config=None, **kwargs):
+ """Execute self-tests.
+
+ This raises SelfTestError if any test is unsuccessful.
+
+ You may optionally pass in a sub-module of SelfTest if you only want to
+ perform some of the tests. For example, the following would test only the
+ hash modules:
+
+ Crypto.SelfTest.run(Crypto.SelfTest.Hash)
+
+ """
+
+ if config is None:
+ config = {}
+ suite = unittest.TestSuite()
+ if module is None:
+ if tests is None:
+ tests = get_tests(config=config)
+ suite.addTests(tests)
+ else:
+ if tests is None:
+ suite.addTests(module.get_tests(config=config))
+ else:
+ raise ValueError("'module' and 'tests' arguments are mutually exclusive")
+ if stream is None:
+ kwargs['stream'] = StringIO()
+ else:
+ kwargs['stream'] = stream
+ runner = unittest.TextTestRunner(verbosity=verbosity, **kwargs)
+ result = runner.run(suite)
+ if not result.wasSuccessful():
+ if stream is None:
+ sys.stderr.write(kwargs['stream'].getvalue())
+ raise SelfTestError("Self-test failed", result)
+ return result
+
+
+def get_tests(config={}):
+ tests = []
+
+ module_names = [
+ "Cipher", "Hash", "Protocol", "PublicKey", "Random",
+ "Util", "Signature", "IO", "Math",
+ ]
+
+ for name in module_names:
+ module = import_module("Crypto.SelfTest." + name)
+ tests += module.get_tests(config=config)
+
+ return tests
+
+
+if __name__ == '__main__':
+ def suite():
+ return unittest.TestSuite(get_tests())
+ unittest.main(defaultTest='suite')
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/__main__.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/__main__.py
new file mode 100644
index 00000000..537254ab
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/__main__.py
@@ -0,0 +1,43 @@
+#! /usr/bin/env python
+#
+# __main__.py : Stand-along loader for PyCryptodome test suite
+#
+# ===================================================================
+# 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 __future__ import print_function
+
+import sys
+
+from Crypto import SelfTest
+
+slow_tests = not ("--skip-slow-tests" in sys.argv)
+if not slow_tests:
+ print("Skipping slow tests")
+
+wycheproof_warnings = "--wycheproof-warnings" in sys.argv
+if wycheproof_warnings:
+ print("Printing Wycheproof warnings")
+
+if "-v" in sys.argv:
+ verbosity=2
+else:
+ verbosity=1
+
+config = {'slow_tests': slow_tests, 'wycheproof_warnings': wycheproof_warnings}
+SelfTest.run(stream=sys.stdout, verbosity=verbosity, config=config)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/loader.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/loader.py
new file mode 100644
index 00000000..16bc844b
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/loader.py
@@ -0,0 +1,250 @@
+# ===================================================================
+#
+# Copyright (c) 2016, Legrandin
+# 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 os
+import re
+import json
+import errno
+import binascii
+import warnings
+from binascii import unhexlify
+from Crypto.Util.py3compat import FileNotFoundError
+
+
+try:
+ import pycryptodome_test_vectors # type: ignore
+ test_vectors_available = True
+except ImportError:
+ test_vectors_available = False
+
+
+def _load_tests(dir_comps, file_in, description, conversions):
+ """Load and parse a test vector file
+
+ Return a list of objects, one per group of adjacent
+ KV lines or for a single line in the form "[.*]".
+
+ For a group of lines, the object has one attribute per line.
+ """
+
+ line_number = 0
+ results = []
+
+ class TestVector(object):
+ def __init__(self, description, count):
+ self.desc = description
+ self.count = count
+ self.others = []
+
+ test_vector = None
+ count = 0
+ new_group = True
+
+ while True:
+ line_number += 1
+ line = file_in.readline()
+ if not line:
+ if test_vector is not None:
+ results.append(test_vector)
+ break
+ line = line.strip()
+
+ # Skip comments and empty lines
+ if line.startswith('#') or not line:
+ new_group = True
+ continue
+
+ if line.startswith("["):
+ if test_vector is not None:
+ results.append(test_vector)
+ test_vector = None
+ results.append(line)
+ continue
+
+ if new_group:
+ count += 1
+ new_group = False
+ if test_vector is not None:
+ results.append(test_vector)
+ test_vector = TestVector("%s (#%d)" % (description, count), count)
+
+ res = re.match("([A-Za-z0-9]+) = ?(.*)", line)
+ if not res:
+ test_vector.others += [line]
+ else:
+ token = res.group(1).lower()
+ data = res.group(2).lower()
+
+ conversion = conversions.get(token, None)
+ if conversion is None:
+ if len(data) % 2 != 0:
+ data = "0" + data
+ setattr(test_vector, token, binascii.unhexlify(data))
+ else:
+ setattr(test_vector, token, conversion(data))
+
+ # This line is ignored
+ return results
+
+
+def load_test_vectors(dir_comps, file_name, description, conversions):
+ """Load and parse a test vector file, formatted using the NIST style.
+
+ Args:
+ dir_comps (list of strings):
+ The path components under the ``pycryptodome_test_vectors`` package.
+ For instance ``("Cipher", "AES")``.
+ file_name (string):
+ The name of the file with the test vectors.
+ description (string):
+ A description applicable to the test vectors in the file.
+ conversions (dictionary):
+ The dictionary contains functions.
+ Values in the file that have an entry in this dictionary
+ will be converted usign the matching function.
+ Otherwise, values will be considered as hexadecimal and
+ converted to binary.
+
+ Returns:
+ A list of test vector objects.
+
+ The file is formatted in the following way:
+
+ - Lines starting with "#" are comments and will be ignored.
+ - Each test vector is a sequence of 1 or more adjacent lines, where
+ each lines is an assignement.
+ - Test vectors are separated by an empty line, a comment, or
+ a line starting with "[".
+
+ A test vector object has the following attributes:
+
+ - desc (string): description
+ - counter (int): the order of the test vector in the file (from 1)
+ - others (list): zero or more lines of the test vector that were not assignments
+ - left-hand side of each assignment (lowercase): the value of the
+ assignement, either converted or bytes.
+ """
+
+ results = None
+
+ try:
+ if not test_vectors_available:
+ raise FileNotFoundError(errno.ENOENT,
+ os.strerror(errno.ENOENT),
+ file_name)
+
+ description = "%s test (%s)" % (description, file_name)
+
+ init_dir = os.path.dirname(pycryptodome_test_vectors.__file__)
+ full_file_name = os.path.join(os.path.join(init_dir, *dir_comps), file_name)
+ with open(full_file_name) as file_in:
+ results = _load_tests(dir_comps, file_in, description, conversions)
+
+ except FileNotFoundError:
+ warnings.warn("Warning: skipping extended tests for " + description,
+ UserWarning,
+ stacklevel=2)
+
+ return results
+
+
+def load_test_vectors_wycheproof(dir_comps, file_name, description,
+ root_tag={}, group_tag={}, unit_tag={}):
+
+ result = []
+ try:
+ if not test_vectors_available:
+ raise FileNotFoundError(errno.ENOENT,
+ os.strerror(errno.ENOENT),
+ file_name)
+
+ init_dir = os.path.dirname(pycryptodome_test_vectors.__file__)
+ full_file_name = os.path.join(os.path.join(init_dir, *dir_comps), file_name)
+ with open(full_file_name) as file_in:
+ tv_tree = json.load(file_in)
+
+ except FileNotFoundError:
+ warnings.warn("Warning: skipping extended tests for " + description,
+ UserWarning,
+ stacklevel=2)
+ return result
+
+ class TestVector(object):
+ pass
+
+ # Unique attributes that will be converted from
+ # hexadecimal to binary, unless the attribute is
+ # listed in the unit_tag dict
+ unit_attr_hex = {'key', 'iv', 'aad', 'msg', 'ct', 'tag', 'label',
+ 'ikm', 'salt', 'info', 'okm', 'sig', 'public',
+ 'shared'}
+ unit_attr_hex -= set(unit_tag.keys())
+
+ common_root = {}
+ for k, v in root_tag.items():
+ common_root[k] = v(tv_tree)
+
+ for group in tv_tree['testGroups']:
+
+ common_group = {}
+ for k, v in group_tag.items():
+ common_group[k] = v(group)
+
+ for test in group['tests']:
+ tv = TestVector()
+
+ for k, v in common_root.items():
+ setattr(tv, k, v)
+ for k, v in common_group.items():
+ setattr(tv, k, v)
+
+ tv.id = test['tcId']
+ tv.comment = test['comment']
+ for attr in unit_attr_hex:
+ if attr in test:
+ try:
+ setattr(tv, attr, unhexlify(test[attr]))
+ except binascii.Error:
+ raise ValueError("Error decoding attribute '%s' (tcId=%s, file %s)" % (attr, tv.id, file_name))
+ tv.filename = file_name
+
+ for k, v in unit_tag.items():
+ setattr(tv, k, v(test))
+
+ tv.valid = test['result'] != "invalid"
+ tv.warning = test['result'] == "acceptable"
+ tv.flags = test.get('flags')
+
+ tv.filename = file_name
+
+ result.append(tv)
+
+ return result
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/st_common.py b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/st_common.py
new file mode 100644
index 00000000..e098d812
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/SelfTest/st_common.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+#
+# SelfTest/st_common.py: Common functions for SelfTest modules
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Common functions for SelfTest modules"""
+
+import unittest
+import binascii
+from Crypto.Util.py3compat import b
+
+
+def list_test_cases(class_):
+ """Return a list of TestCase instances given a TestCase class
+
+ This is useful when you have defined test* methods on your TestCase class.
+ """
+ return unittest.TestLoader().loadTestsFromTestCase(class_)
+
+def strip_whitespace(s):
+ """Remove whitespace from a text or byte string"""
+ if isinstance(s,str):
+ return b("".join(s.split()))
+ else:
+ return b("").join(s.split())
+
+def a2b_hex(s):
+ """Convert hexadecimal to binary, ignoring whitespace"""
+ return binascii.a2b_hex(strip_whitespace(s))
+
+def b2a_hex(s):
+ """Convert binary to hexadecimal"""
+ # For completeness
+ return binascii.b2a_hex(s)
+
+# vim:set ts=4 sw=4 sts=4 expandtab:
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/DSS.py b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/DSS.py
new file mode 100644
index 00000000..80177742
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/DSS.py
@@ -0,0 +1,403 @@
+#
+# Signature/DSS.py : DSS.py
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.Util.asn1 import DerSequence
+from Crypto.Util.number import long_to_bytes
+from Crypto.Math.Numbers import Integer
+
+from Crypto.Hash import HMAC
+from Crypto.PublicKey.ECC import EccKey
+from Crypto.PublicKey.DSA import DsaKey
+
+__all__ = ['DssSigScheme', 'new']
+
+
+class DssSigScheme(object):
+ """A (EC)DSA signature object.
+ Do not instantiate directly.
+ Use :func:`Crypto.Signature.DSS.new`.
+ """
+
+ def __init__(self, key, encoding, order):
+ """Create a new Digital Signature Standard (DSS) object.
+
+ Do not instantiate this object directly,
+ use `Crypto.Signature.DSS.new` instead.
+ """
+
+ self._key = key
+ self._encoding = encoding
+ self._order = order
+
+ self._order_bits = self._order.size_in_bits()
+ self._order_bytes = (self._order_bits - 1) // 8 + 1
+
+ def can_sign(self):
+ """Return ``True`` if this signature object can be used
+ for signing messages."""
+
+ return self._key.has_private()
+
+ def _compute_nonce(self, msg_hash):
+ raise NotImplementedError("To be provided by subclasses")
+
+ def _valid_hash(self, msg_hash):
+ raise NotImplementedError("To be provided by subclasses")
+
+ def sign(self, msg_hash):
+ """Compute the DSA/ECDSA signature of a message.
+
+ Args:
+ msg_hash (hash object):
+ The hash that was carried out over the message.
+ The object belongs to the :mod:`Crypto.Hash` package.
+ Under mode ``'fips-186-3'``, the hash must be a FIPS
+ approved secure hash (SHA-2 or SHA-3).
+
+ :return: The signature as ``bytes``
+ :raise ValueError: if the hash algorithm is incompatible to the (EC)DSA key
+ :raise TypeError: if the (EC)DSA key has no private half
+ """
+
+ if not self._key.has_private():
+ raise TypeError("Private key is needed to sign")
+
+ if not self._valid_hash(msg_hash):
+ raise ValueError("Hash is not sufficiently strong")
+
+ # Generate the nonce k (critical!)
+ nonce = self._compute_nonce(msg_hash)
+
+ # Perform signature using the raw API
+ z = Integer.from_bytes(msg_hash.digest()[:self._order_bytes])
+ sig_pair = self._key._sign(z, nonce)
+
+ # Encode the signature into a single byte string
+ if self._encoding == 'binary':
+ output = b"".join([long_to_bytes(x, self._order_bytes)
+ for x in sig_pair])
+ else:
+ # Dss-sig ::= SEQUENCE {
+ # r INTEGER,
+ # s INTEGER
+ # }
+ # Ecdsa-Sig-Value ::= SEQUENCE {
+ # r INTEGER,
+ # s INTEGER
+ # }
+ output = DerSequence(sig_pair).encode()
+
+ return output
+
+ def verify(self, msg_hash, signature):
+ """Check if a certain (EC)DSA signature is authentic.
+
+ Args:
+ msg_hash (hash object):
+ The hash that was carried out over the message.
+ This is an object belonging to the :mod:`Crypto.Hash` module.
+ Under mode ``'fips-186-3'``, the hash must be a FIPS
+ approved secure hash (SHA-2 or SHA-3).
+
+ signature (``bytes``):
+ The signature that needs to be validated.
+
+ :raise ValueError: if the signature is not authentic
+ """
+
+ if not self._valid_hash(msg_hash):
+ raise ValueError("Hash is not sufficiently strong")
+
+ if self._encoding == 'binary':
+ if len(signature) != (2 * self._order_bytes):
+ raise ValueError("The signature is not authentic (length)")
+ r_prime, s_prime = [Integer.from_bytes(x)
+ for x in (signature[:self._order_bytes],
+ signature[self._order_bytes:])]
+ else:
+ try:
+ der_seq = DerSequence().decode(signature, strict=True)
+ except (ValueError, IndexError):
+ raise ValueError("The signature is not authentic (DER)")
+ if len(der_seq) != 2 or not der_seq.hasOnlyInts():
+ raise ValueError("The signature is not authentic (DER content)")
+ r_prime, s_prime = Integer(der_seq[0]), Integer(der_seq[1])
+
+ if not (0 < r_prime < self._order) or not (0 < s_prime < self._order):
+ raise ValueError("The signature is not authentic (d)")
+
+ z = Integer.from_bytes(msg_hash.digest()[:self._order_bytes])
+ result = self._key._verify(z, (r_prime, s_prime))
+ if not result:
+ raise ValueError("The signature is not authentic")
+ # Make PyCrypto code to fail
+ return False
+
+
+class DeterministicDsaSigScheme(DssSigScheme):
+ # Also applicable to ECDSA
+
+ def __init__(self, key, encoding, order, private_key):
+ super(DeterministicDsaSigScheme, self).__init__(key, encoding, order)
+ self._private_key = private_key
+
+ def _bits2int(self, bstr):
+ """See 2.3.2 in RFC6979"""
+
+ result = Integer.from_bytes(bstr)
+ q_len = self._order.size_in_bits()
+ b_len = len(bstr) * 8
+ if b_len > q_len:
+ # Only keep leftmost q_len bits
+ result >>= (b_len - q_len)
+ return result
+
+ def _int2octets(self, int_mod_q):
+ """See 2.3.3 in RFC6979"""
+
+ assert 0 < int_mod_q < self._order
+ return long_to_bytes(int_mod_q, self._order_bytes)
+
+ def _bits2octets(self, bstr):
+ """See 2.3.4 in RFC6979"""
+
+ z1 = self._bits2int(bstr)
+ if z1 < self._order:
+ z2 = z1
+ else:
+ z2 = z1 - self._order
+ return self._int2octets(z2)
+
+ def _compute_nonce(self, mhash):
+ """Generate k in a deterministic way"""
+
+ # See section 3.2 in RFC6979.txt
+ # Step a
+ h1 = mhash.digest()
+ # Step b
+ mask_v = b'\x01' * mhash.digest_size
+ # Step c
+ nonce_k = b'\x00' * mhash.digest_size
+
+ for int_oct in (b'\x00', b'\x01'):
+ # Step d/f
+ nonce_k = HMAC.new(nonce_k,
+ mask_v + int_oct +
+ self._int2octets(self._private_key) +
+ self._bits2octets(h1), mhash).digest()
+ # Step e/g
+ mask_v = HMAC.new(nonce_k, mask_v, mhash).digest()
+
+ nonce = -1
+ while not (0 < nonce < self._order):
+ # Step h.C (second part)
+ if nonce != -1:
+ nonce_k = HMAC.new(nonce_k, mask_v + b'\x00',
+ mhash).digest()
+ mask_v = HMAC.new(nonce_k, mask_v, mhash).digest()
+
+ # Step h.A
+ mask_t = b""
+
+ # Step h.B
+ while len(mask_t) < self._order_bytes:
+ mask_v = HMAC.new(nonce_k, mask_v, mhash).digest()
+ mask_t += mask_v
+
+ # Step h.C (first part)
+ nonce = self._bits2int(mask_t)
+ return nonce
+
+ def _valid_hash(self, msg_hash):
+ return True
+
+
+class FipsDsaSigScheme(DssSigScheme):
+
+ #: List of L (bit length of p) and N (bit length of q) combinations
+ #: that are allowed by FIPS 186-3. The security level is provided in
+ #: Table 2 of FIPS 800-57 (rev3).
+ _fips_186_3_L_N = (
+ (1024, 160), # 80 bits (SHA-1 or stronger)
+ (2048, 224), # 112 bits (SHA-224 or stronger)
+ (2048, 256), # 128 bits (SHA-256 or stronger)
+ (3072, 256) # 256 bits (SHA-512)
+ )
+
+ def __init__(self, key, encoding, order, randfunc):
+ super(FipsDsaSigScheme, self).__init__(key, encoding, order)
+ self._randfunc = randfunc
+
+ L = Integer(key.p).size_in_bits()
+ if (L, self._order_bits) not in self._fips_186_3_L_N:
+ error = ("L/N (%d, %d) is not compliant to FIPS 186-3"
+ % (L, self._order_bits))
+ raise ValueError(error)
+
+ def _compute_nonce(self, msg_hash):
+ # hash is not used
+ return Integer.random_range(min_inclusive=1,
+ max_exclusive=self._order,
+ randfunc=self._randfunc)
+
+ def _valid_hash(self, msg_hash):
+ """Verify that SHA-1, SHA-2 or SHA-3 are used"""
+ return (msg_hash.oid == "1.3.14.3.2.26" or
+ msg_hash.oid.startswith("2.16.840.1.101.3.4.2."))
+
+
+class FipsEcDsaSigScheme(DssSigScheme):
+
+ def __init__(self, key, encoding, order, randfunc):
+ super(FipsEcDsaSigScheme, self).__init__(key, encoding, order)
+ self._randfunc = randfunc
+
+ def _compute_nonce(self, msg_hash):
+ return Integer.random_range(min_inclusive=1,
+ max_exclusive=self._key._curve.order,
+ randfunc=self._randfunc)
+
+ def _valid_hash(self, msg_hash):
+ """Verify that the strength of the hash matches or exceeds
+ the strength of the EC. We fail if the hash is too weak."""
+
+ modulus_bits = self._key.pointQ.size_in_bits()
+
+ # SHS: SHA-2, SHA-3, truncated SHA-512
+ sha224 = ("2.16.840.1.101.3.4.2.4", "2.16.840.1.101.3.4.2.7", "2.16.840.1.101.3.4.2.5")
+ sha256 = ("2.16.840.1.101.3.4.2.1", "2.16.840.1.101.3.4.2.8", "2.16.840.1.101.3.4.2.6")
+ sha384 = ("2.16.840.1.101.3.4.2.2", "2.16.840.1.101.3.4.2.9")
+ sha512 = ("2.16.840.1.101.3.4.2.3", "2.16.840.1.101.3.4.2.10")
+ shs = sha224 + sha256 + sha384 + sha512
+
+ try:
+ result = msg_hash.oid in shs
+ except AttributeError:
+ result = False
+ return result
+
+
+def new(key, mode, encoding='binary', randfunc=None):
+ """Create a signature object :class:`DssSigScheme` that
+ can perform (EC)DSA signature or verification.
+
+ .. note::
+ Refer to `NIST SP 800 Part 1 Rev 4`_ (or newer release) for an
+ overview of the recommended key lengths.
+
+ Args:
+ key (:class:`Crypto.PublicKey.DSA` or :class:`Crypto.PublicKey.ECC`):
+ The key to use for computing the signature (*private* keys only)
+ or for verifying one.
+ For DSA keys, let ``L`` and ``N`` be the bit lengths of the modulus ``p``
+ and of ``q``: the pair ``(L,N)`` must appear in the following list,
+ in compliance to section 4.2 of `FIPS 186-4`_:
+
+ - (1024, 160) *legacy only; do not create new signatures with this*
+ - (2048, 224) *deprecated; do not create new signatures with this*
+ - (2048, 256)
+ - (3072, 256)
+
+ For ECC, only keys over P-224, P-256, P-384, and P-521 are accepted.
+
+ mode (string):
+ The parameter can take these values:
+
+ - ``'fips-186-3'``. The signature generation is randomized and carried out
+ according to `FIPS 186-3`_: the nonce ``k`` is taken from the RNG.
+ - ``'deterministic-rfc6979'``. The signature generation is not
+ randomized. See RFC6979_.
+
+ encoding (string):
+ How the signature is encoded. This value determines the output of
+ :meth:`sign` and the input to :meth:`verify`.
+
+ The following values are accepted:
+
+ - ``'binary'`` (default), the signature is the raw concatenation
+ of ``r`` and ``s``. It is defined in the IEEE P.1363 standard.
+ For DSA, the size in bytes of the signature is ``N/4`` bytes
+ (e.g. 64 for ``N=256``).
+ For ECDSA, the signature is always twice the length of a point
+ coordinate (e.g. 64 bytes for P-256).
+
+ - ``'der'``, the signature is a ASN.1 DER SEQUENCE
+ with two INTEGERs (``r`` and ``s``). It is defined in RFC3279_.
+ The size of the signature is variable.
+
+ randfunc (callable):
+ A function that returns random ``bytes``, of a given length.
+ If omitted, the internal RNG is used.
+ Only applicable for the *'fips-186-3'* mode.
+
+ .. _FIPS 186-3: http://csrc.nist.gov/publications/fips/fips186-3/fips_186-3.pdf
+ .. _FIPS 186-4: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf
+ .. _NIST SP 800 Part 1 Rev 4: http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r4.pdf
+ .. _RFC6979: http://tools.ietf.org/html/rfc6979
+ .. _RFC3279: https://tools.ietf.org/html/rfc3279#section-2.2.2
+ """
+
+ # The goal of the 'mode' parameter is to avoid to
+ # have the current version of the standard as default.
+ #
+ # Over time, such version will be superseded by (for instance)
+ # FIPS 186-4 and it will be odd to have -3 as default.
+
+ if encoding not in ('binary', 'der'):
+ raise ValueError("Unknown encoding '%s'" % encoding)
+
+ if isinstance(key, EccKey):
+ order = key._curve.order
+ private_key_attr = 'd'
+ if not key.curve.startswith("NIST"):
+ raise ValueError("ECC key is not on a NIST P curve")
+ elif isinstance(key, DsaKey):
+ order = Integer(key.q)
+ private_key_attr = 'x'
+ else:
+ raise ValueError("Unsupported key type " + str(type(key)))
+
+ if key.has_private():
+ private_key = getattr(key, private_key_attr)
+ else:
+ private_key = None
+
+ if mode == 'deterministic-rfc6979':
+ return DeterministicDsaSigScheme(key, encoding, order, private_key)
+ elif mode == 'fips-186-3':
+ if isinstance(key, EccKey):
+ return FipsEcDsaSigScheme(key, encoding, order, randfunc)
+ else:
+ return FipsDsaSigScheme(key, encoding, order, randfunc)
+ else:
+ raise ValueError("Unknown DSS mode '%s'" % mode)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/DSS.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/DSS.pyi
new file mode 100644
index 00000000..08cad814
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/DSS.pyi
@@ -0,0 +1,27 @@
+from typing import Union, Optional, Callable
+from typing_extensions import Protocol
+
+from Crypto.PublicKey.DSA import DsaKey
+from Crypto.PublicKey.ECC import EccKey
+
+class Hash(Protocol):
+ def digest(self) -> bytes: ...
+
+__all__ = ['new']
+
+class DssSigScheme:
+ def __init__(self, key: Union[DsaKey, EccKey], encoding: str, order: int) -> None: ...
+ def can_sign(self) -> bool: ...
+ def sign(self, msg_hash: Hash) -> bytes: ...
+ def verify(self, msg_hash: Hash, signature: bytes) -> bool: ...
+
+class DeterministicDsaSigScheme(DssSigScheme):
+ def __init__(self, key, encoding, order, private_key) -> None: ...
+
+class FipsDsaSigScheme(DssSigScheme):
+ def __init__(self, key: DsaKey, encoding: str, order: int, randfunc: Callable) -> None: ...
+
+class FipsEcDsaSigScheme(DssSigScheme):
+ def __init__(self, key: EccKey, encoding: str, order: int, randfunc: Callable) -> None: ...
+
+def new(key: Union[DsaKey, EccKey], mode: str, encoding: Optional[str]='binary', randfunc: Optional[Callable]=None) -> Union[DeterministicDsaSigScheme, FipsDsaSigScheme, FipsEcDsaSigScheme]: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_PSS.py b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_PSS.py
new file mode 100644
index 00000000..c39d3881
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_PSS.py
@@ -0,0 +1,55 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""
+Legacy module for PKCS#1 PSS signatures.
+
+:undocumented: __package__
+"""
+
+import types
+
+from Crypto.Signature import pss
+
+
+def _pycrypto_verify(self, hash_object, signature):
+ try:
+ self._verify(hash_object, signature)
+ except (ValueError, TypeError):
+ return False
+ return True
+
+
+def new(rsa_key, mgfunc=None, saltLen=None, randfunc=None):
+ pkcs1 = pss.new(rsa_key, mask_func=mgfunc,
+ salt_bytes=saltLen, rand_func=randfunc)
+ pkcs1._verify = pkcs1.verify
+ pkcs1.verify = types.MethodType(_pycrypto_verify, pkcs1)
+ return pkcs1
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_PSS.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_PSS.pyi
new file mode 100644
index 00000000..1371e69a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_PSS.pyi
@@ -0,0 +1,28 @@
+from typing import Union, Callable, Optional
+from typing_extensions import Protocol
+
+from Crypto.PublicKey.RSA import RsaKey
+
+
+class Hash(Protocol):
+ def digest(self) -> bytes: ...
+ def update(self, bytes) -> None: ...
+
+
+class HashModule(Protocol):
+ @staticmethod
+ def new(data: Optional[bytes]) -> Hash: ...
+
+
+MaskFunction = Callable[[bytes, int, Union[Hash, HashModule]], bytes]
+RndFunction = Callable[[int], bytes]
+
+class PSS_SigScheme:
+ def __init__(self, key: RsaKey, mgfunc: MaskFunction, saltLen: int, randfunc: RndFunction) -> None: ...
+ def can_sign(self) -> bool: ...
+ def sign(self, msg_hash: Hash) -> bytes: ...
+ def verify(self, msg_hash: Hash, signature: bytes) -> bool: ...
+
+
+
+def new(rsa_key: RsaKey, mgfunc: Optional[MaskFunction]=None, saltLen: Optional[int]=None, randfunc: Optional[RndFunction]=None) -> PSS_SigScheme: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_v1_5.py b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_v1_5.py
new file mode 100644
index 00000000..ac888edb
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_v1_5.py
@@ -0,0 +1,53 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""
+Legacy module for PKCS#1 v1.5 signatures.
+
+:undocumented: __package__
+"""
+
+import types
+
+from Crypto.Signature import pkcs1_15
+
+def _pycrypto_verify(self, hash_object, signature):
+ try:
+ self._verify(hash_object, signature)
+ except (ValueError, TypeError):
+ return False
+ return True
+
+def new(rsa_key):
+ pkcs1 = pkcs1_15.new(rsa_key)
+ pkcs1._verify = pkcs1.verify
+ pkcs1.verify = types.MethodType(_pycrypto_verify, pkcs1)
+ return pkcs1
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_v1_5.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_v1_5.pyi
new file mode 100644
index 00000000..6e9d8eda
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/PKCS1_v1_5.pyi
@@ -0,0 +1,16 @@
+from typing import Optional
+from typing_extensions import Protocol
+
+from Crypto.PublicKey.RSA import RsaKey
+
+class Hash(Protocol):
+ def digest(self) -> bytes: ...
+
+class PKCS115_SigScheme:
+ def __init__(self, rsa_key: RsaKey) -> None: ...
+ def can_sign(self) -> bool: ...
+ def sign(self, msg_hash: Hash) -> bytes: ...
+ def verify(self, msg_hash: Hash, signature: bytes) -> bool: ...
+
+
+def new(rsa_key: RsaKey) -> PKCS115_SigScheme: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/__init__.py
new file mode 100644
index 00000000..11ca64c3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/__init__.py
@@ -0,0 +1,36 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+"""Digital signature protocols
+
+A collection of standardized protocols to carry out digital signatures.
+"""
+
+__all__ = ['PKCS1_v1_5', 'PKCS1_PSS', 'DSS', 'pkcs1_15', 'pss', 'eddsa']
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/eddsa.py b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/eddsa.py
new file mode 100644
index 00000000..0396b28f
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/eddsa.py
@@ -0,0 +1,343 @@
+# ===================================================================
+#
+# Copyright (c) 2022, Legrandin
+# 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.Math.Numbers import Integer
+
+from Crypto.Hash import SHA512, SHAKE256
+from Crypto.Util.py3compat import bchr, is_bytes
+from Crypto.PublicKey.ECC import (EccKey,
+ construct,
+ _import_ed25519_public_key,
+ _import_ed448_public_key)
+
+
+def import_public_key(encoded):
+ """Create a new Ed25519 or Ed448 public key object,
+ starting from the key encoded as raw ``bytes``,
+ in the format described in RFC8032.
+
+ Args:
+ encoded (bytes):
+ The EdDSA public key to import.
+ It must be 32 bytes for Ed25519, and 57 bytes for Ed448.
+
+ Returns:
+ :class:`Crypto.PublicKey.EccKey` : a new ECC key object.
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+ """
+
+ if len(encoded) == 32:
+ x, y = _import_ed25519_public_key(encoded)
+ curve_name = "Ed25519"
+ elif len(encoded) == 57:
+ x, y = _import_ed448_public_key(encoded)
+ curve_name = "Ed448"
+ else:
+ raise ValueError("Not an EdDSA key (%d bytes)" % len(encoded))
+ return construct(curve=curve_name, point_x=x, point_y=y)
+
+
+def import_private_key(encoded):
+ """Create a new Ed25519 or Ed448 private key object,
+ starting from the key encoded as raw ``bytes``,
+ in the format described in RFC8032.
+
+ Args:
+ encoded (bytes):
+ The EdDSA private key to import.
+ It must be 32 bytes for Ed25519, and 57 bytes for Ed448.
+
+ Returns:
+ :class:`Crypto.PublicKey.EccKey` : a new ECC key object.
+
+ Raises:
+ ValueError: when the given key cannot be parsed.
+ """
+
+ if len(encoded) == 32:
+ curve_name = "ed25519"
+ elif len(encoded) == 57:
+ curve_name = "ed448"
+ else:
+ raise ValueError("Incorrect length. Only EdDSA private keys are supported.")
+
+ # Note that the private key is truly a sequence of random bytes,
+ # so we cannot check its correctness in any way.
+
+ return construct(seed=encoded, curve=curve_name)
+
+
+class EdDSASigScheme(object):
+ """An EdDSA signature object.
+ Do not instantiate directly.
+ Use :func:`Crypto.Signature.eddsa.new`.
+ """
+
+ def __init__(self, key, context):
+ """Create a new EdDSA object.
+
+ Do not instantiate this object directly,
+ use `Crypto.Signature.DSS.new` instead.
+ """
+
+ self._key = key
+ self._context = context
+ self._A = key._export_eddsa_public()
+ self._order = key._curve.order
+
+ def can_sign(self):
+ """Return ``True`` if this signature object can be used
+ for signing messages."""
+
+ return self._key.has_private()
+
+ def sign(self, msg_or_hash):
+ """Compute the EdDSA signature of a message.
+
+ Args:
+ msg_or_hash (bytes or a hash object):
+ The message to sign (``bytes``, in case of *PureEdDSA*) or
+ the hash that was carried out over the message (hash object, for *HashEdDSA*).
+
+ The hash object must be :class:`Crypto.Hash.SHA512` for Ed25519,
+ and :class:`Crypto.Hash.SHAKE256` object for Ed448.
+
+ :return: The signature as ``bytes``. It is always 64 bytes for Ed25519, and 114 bytes for Ed448.
+ :raise TypeError: if the EdDSA key has no private half
+ """
+
+ if not self._key.has_private():
+ raise TypeError("Private key is needed to sign")
+
+ if self._key.curve == "Ed25519":
+ ph = isinstance(msg_or_hash, SHA512.SHA512Hash)
+ if not (ph or is_bytes(msg_or_hash)):
+ raise TypeError("'msg_or_hash' must be bytes of a SHA-512 hash")
+ eddsa_sign_method = self._sign_ed25519
+
+ elif self._key.curve == "Ed448":
+ ph = isinstance(msg_or_hash, SHAKE256.SHAKE256_XOF)
+ if not (ph or is_bytes(msg_or_hash)):
+ raise TypeError("'msg_or_hash' must be bytes of a SHAKE256 hash")
+ eddsa_sign_method = self._sign_ed448
+
+ else:
+ raise ValueError("Incorrect curve for EdDSA")
+
+ return eddsa_sign_method(msg_or_hash, ph)
+
+ def _sign_ed25519(self, msg_or_hash, ph):
+
+ if self._context or ph:
+ flag = int(ph)
+ # dom2(flag, self._context)
+ dom2 = b'SigEd25519 no Ed25519 collisions' + bchr(flag) + \
+ bchr(len(self._context)) + self._context
+ else:
+ dom2 = b''
+
+ PHM = msg_or_hash.digest() if ph else msg_or_hash
+
+ # See RFC 8032, section 5.1.6
+
+ # Step 2
+ r_hash = SHA512.new(dom2 + self._key._prefix + PHM).digest()
+ r = Integer.from_bytes(r_hash, 'little') % self._order
+ # Step 3
+ R_pk = EccKey(point=r * self._key._curve.G)._export_eddsa_public()
+ # Step 4
+ k_hash = SHA512.new(dom2 + R_pk + self._A + PHM).digest()
+ k = Integer.from_bytes(k_hash, 'little') % self._order
+ # Step 5
+ s = (r + k * self._key.d) % self._order
+
+ return R_pk + s.to_bytes(32, 'little')
+
+ def _sign_ed448(self, msg_or_hash, ph):
+
+ flag = int(ph)
+ # dom4(flag, self._context)
+ dom4 = b'SigEd448' + bchr(flag) + \
+ bchr(len(self._context)) + self._context
+
+ PHM = msg_or_hash.copy().read(64) if ph else msg_or_hash
+
+ # See RFC 8032, section 5.2.6
+
+ # Step 2
+ r_hash = SHAKE256.new(dom4 + self._key._prefix + PHM).read(114)
+ r = Integer.from_bytes(r_hash, 'little') % self._order
+ # Step 3
+ R_pk = EccKey(point=r * self._key._curve.G)._export_eddsa_public()
+ # Step 4
+ k_hash = SHAKE256.new(dom4 + R_pk + self._A + PHM).read(114)
+ k = Integer.from_bytes(k_hash, 'little') % self._order
+ # Step 5
+ s = (r + k * self._key.d) % self._order
+
+ return R_pk + s.to_bytes(57, 'little')
+
+ def verify(self, msg_or_hash, signature):
+ """Check if an EdDSA signature is authentic.
+
+ Args:
+ msg_or_hash (bytes or a hash object):
+ The message to verify (``bytes``, in case of *PureEdDSA*) or
+ the hash that was carried out over the message (hash object, for *HashEdDSA*).
+
+ The hash object must be :class:`Crypto.Hash.SHA512` object for Ed25519,
+ and :class:`Crypto.Hash.SHAKE256` for Ed448.
+
+ signature (``bytes``):
+ The signature that needs to be validated.
+ It must be 64 bytes for Ed25519, and 114 bytes for Ed448.
+
+ :raise ValueError: if the signature is not authentic
+ """
+
+ if self._key.curve == "Ed25519":
+ ph = isinstance(msg_or_hash, SHA512.SHA512Hash)
+ if not (ph or is_bytes(msg_or_hash)):
+ raise TypeError("'msg_or_hash' must be bytes of a SHA-512 hash")
+ eddsa_verify_method = self._verify_ed25519
+
+ elif self._key.curve == "Ed448":
+ ph = isinstance(msg_or_hash, SHAKE256.SHAKE256_XOF)
+ if not (ph or is_bytes(msg_or_hash)):
+ raise TypeError("'msg_or_hash' must be bytes of a SHAKE256 hash")
+ eddsa_verify_method = self._verify_ed448
+
+ else:
+ raise ValueError("Incorrect curve for EdDSA")
+
+ return eddsa_verify_method(msg_or_hash, signature, ph)
+
+ def _verify_ed25519(self, msg_or_hash, signature, ph):
+
+ if len(signature) != 64:
+ raise ValueError("The signature is not authentic (length)")
+
+ if self._context or ph:
+ flag = int(ph)
+ dom2 = b'SigEd25519 no Ed25519 collisions' + bchr(flag) + \
+ bchr(len(self._context)) + self._context
+ else:
+ dom2 = b''
+
+ PHM = msg_or_hash.digest() if ph else msg_or_hash
+
+ # Section 5.1.7
+
+ # Step 1
+ try:
+ R = import_public_key(signature[:32]).pointQ
+ except ValueError:
+ raise ValueError("The signature is not authentic (R)")
+ s = Integer.from_bytes(signature[32:], 'little')
+ if s > self._order:
+ raise ValueError("The signature is not authentic (S)")
+ # Step 2
+ k_hash = SHA512.new(dom2 + signature[:32] + self._A + PHM).digest()
+ k = Integer.from_bytes(k_hash, 'little') % self._order
+ # Step 3
+ point1 = s * 8 * self._key._curve.G
+ # OPTIMIZE: with double-scalar multiplication, with no SCA
+ # countermeasures because it is public values
+ point2 = 8 * R + k * 8 * self._key.pointQ
+ if point1 != point2:
+ raise ValueError("The signature is not authentic")
+
+ def _verify_ed448(self, msg_or_hash, signature, ph):
+
+ if len(signature) != 114:
+ raise ValueError("The signature is not authentic (length)")
+
+ flag = int(ph)
+ # dom4(flag, self._context)
+ dom4 = b'SigEd448' + bchr(flag) + \
+ bchr(len(self._context)) + self._context
+
+ PHM = msg_or_hash.copy().read(64) if ph else msg_or_hash
+
+ # Section 5.2.7
+
+ # Step 1
+ try:
+ R = import_public_key(signature[:57]).pointQ
+ except ValueError:
+ raise ValueError("The signature is not authentic (R)")
+ s = Integer.from_bytes(signature[57:], 'little')
+ if s > self._order:
+ raise ValueError("The signature is not authentic (S)")
+ # Step 2
+ k_hash = SHAKE256.new(dom4 + signature[:57] + self._A + PHM).read(114)
+ k = Integer.from_bytes(k_hash, 'little') % self._order
+ # Step 3
+ point1 = s * 8 * self._key._curve.G
+ # OPTIMIZE: with double-scalar multiplication, with no SCA
+ # countermeasures because it is public values
+ point2 = 8 * R + k * 8 * self._key.pointQ
+ if point1 != point2:
+ raise ValueError("The signature is not authentic")
+
+
+def new(key, mode, context=None):
+ """Create a signature object :class:`EdDSASigScheme` that
+ can perform or verify an EdDSA signature.
+
+ Args:
+ key (:class:`Crypto.PublicKey.ECC` object):
+ The key to use for computing the signature (*private* keys only)
+ or for verifying one.
+ The key must be on the curve ``Ed25519`` or ``Ed448``.
+
+ mode (string):
+ This parameter must be ``'rfc8032'``.
+
+ context (bytes):
+ Up to 255 bytes of `context `_,
+ which is a constant byte string to segregate different protocols or
+ different applications of the same key.
+ """
+
+ if not isinstance(key, EccKey) or key.curve not in ("Ed25519", "Ed448"):
+ raise ValueError("EdDSA can only be used with EdDSA keys")
+
+ if mode != 'rfc8032':
+ raise ValueError("Mode must be 'rfc8032'")
+
+ if context is None:
+ context = b''
+ elif len(context) > 255:
+ raise ValueError("Context for EdDSA must not be longer than 255 bytes")
+
+ return EdDSASigScheme(key, context)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/eddsa.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/eddsa.pyi
new file mode 100644
index 00000000..060d5938
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/eddsa.pyi
@@ -0,0 +1,21 @@
+from typing import Union, Optional
+from typing_extensions import Protocol
+from Crypto.PublicKey.ECC import EccKey
+
+class Hash(Protocol):
+ def digest(self) -> bytes: ...
+
+class XOF(Protocol):
+ def read(self, len: int) -> bytes: ...
+
+def import_public_key(encoded: bytes) -> EccKey: ...
+def import_private_key(encoded: bytes) -> EccKey: ...
+
+class EdDSASigScheme(object):
+
+ def __init__(self, key: EccKey, context: bytes) -> None: ...
+ def can_sign(self) -> bool: ...
+ def sign(self, msg_or_hash: Union[bytes, Hash, XOF]) -> bytes: ...
+ def verify(self, msg_or_hash: Union[bytes, Hash, XOF], signature: bytes) -> None: ...
+
+def new(key: EccKey, mode: str, context: Optional[bytes]=None) -> EdDSASigScheme: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/pkcs1_15.py b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/pkcs1_15.py
new file mode 100644
index 00000000..0b02d998
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/pkcs1_15.py
@@ -0,0 +1,223 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 Crypto.Util.number
+from Crypto.Util.number import ceil_div, bytes_to_long, long_to_bytes
+from Crypto.Util.asn1 import DerSequence, DerNull, DerOctetString, DerObjectId
+
+class PKCS115_SigScheme:
+ """A signature object for ``RSASSA-PKCS1-v1_5``.
+ Do not instantiate directly.
+ Use :func:`Crypto.Signature.pkcs1_15.new`.
+ """
+
+ def __init__(self, rsa_key):
+ """Initialize this PKCS#1 v1.5 signature scheme object.
+
+ :Parameters:
+ rsa_key : an RSA key object
+ Creation of signatures is only possible if this is a *private*
+ RSA key. Verification of signatures is always possible.
+ """
+ self._key = rsa_key
+
+ def can_sign(self):
+ """Return ``True`` if this object can be used to sign messages."""
+ return self._key.has_private()
+
+ def sign(self, msg_hash):
+ """Create the PKCS#1 v1.5 signature of a message.
+
+ This function is also called ``RSASSA-PKCS1-V1_5-SIGN`` and
+ it is specified in
+ `section 8.2.1 of RFC8017 `_.
+
+ :parameter msg_hash:
+ This is an object from the :mod:`Crypto.Hash` package.
+ It has been used to digest the message to sign.
+ :type msg_hash: hash object
+
+ :return: the signature encoded as a *byte string*.
+ :raise ValueError: if the RSA key is not long enough for the given hash algorithm.
+ :raise TypeError: if the RSA key has no private half.
+ """
+
+ # See 8.2.1 in RFC3447
+ modBits = Crypto.Util.number.size(self._key.n)
+ k = ceil_div(modBits,8) # Convert from bits to bytes
+
+ # Step 1
+ em = _EMSA_PKCS1_V1_5_ENCODE(msg_hash, k)
+ # Step 2a (OS2IP)
+ em_int = bytes_to_long(em)
+ # Step 2b (RSASP1) and Step 2c (I2OSP)
+ signature = self._key._decrypt_to_bytes(em_int)
+ # Verify no faults occurred
+ if em_int != pow(bytes_to_long(signature), self._key.e, self._key.n):
+ raise ValueError("Fault detected in RSA private key operation")
+ return signature
+
+ def verify(self, msg_hash, signature):
+ """Check if the PKCS#1 v1.5 signature over a message is valid.
+
+ This function is also called ``RSASSA-PKCS1-V1_5-VERIFY`` and
+ it is specified in
+ `section 8.2.2 of RFC8037 `_.
+
+ :parameter msg_hash:
+ The hash that was carried out over the message. This is an object
+ belonging to the :mod:`Crypto.Hash` module.
+ :type parameter: hash object
+
+ :parameter signature:
+ The signature that needs to be validated.
+ :type signature: byte string
+
+ :raise ValueError: if the signature is not valid.
+ """
+
+ # See 8.2.2 in RFC3447
+ modBits = Crypto.Util.number.size(self._key.n)
+ k = ceil_div(modBits, 8) # Convert from bits to bytes
+
+ # Step 1
+ if len(signature) != k:
+ raise ValueError("Invalid signature")
+ # Step 2a (O2SIP)
+ signature_int = bytes_to_long(signature)
+ # Step 2b (RSAVP1)
+ em_int = self._key._encrypt(signature_int)
+ # Step 2c (I2OSP)
+ em1 = long_to_bytes(em_int, k)
+ # Step 3
+ try:
+ possible_em1 = [ _EMSA_PKCS1_V1_5_ENCODE(msg_hash, k, True) ]
+ # MD2/4/5 hashes always require NULL params in AlgorithmIdentifier.
+ # For all others, it is optional.
+ try:
+ algorithm_is_md = msg_hash.oid.startswith('1.2.840.113549.2.')
+ except AttributeError:
+ algorithm_is_md = False
+ if not algorithm_is_md: # MD2/MD4/MD5
+ possible_em1.append(_EMSA_PKCS1_V1_5_ENCODE(msg_hash, k, False))
+ except ValueError:
+ raise ValueError("Invalid signature")
+ # Step 4
+ # By comparing the full encodings (as opposed to checking each
+ # of its components one at a time) we avoid attacks to the padding
+ # scheme like Bleichenbacher's (see http://www.mail-archive.com/cryptography@metzdowd.com/msg06537).
+ #
+ if em1 not in possible_em1:
+ raise ValueError("Invalid signature")
+ pass
+
+
+def _EMSA_PKCS1_V1_5_ENCODE(msg_hash, emLen, with_hash_parameters=True):
+ """
+ Implement the ``EMSA-PKCS1-V1_5-ENCODE`` function, as defined
+ in PKCS#1 v2.1 (RFC3447, 9.2).
+
+ ``_EMSA-PKCS1-V1_5-ENCODE`` actually accepts the message ``M`` as input,
+ and hash it internally. Here, we expect that the message has already
+ been hashed instead.
+
+ :Parameters:
+ msg_hash : hash object
+ The hash object that holds the digest of the message being signed.
+ emLen : int
+ The length the final encoding must have, in bytes.
+ with_hash_parameters : bool
+ If True (default), include NULL parameters for the hash
+ algorithm in the ``digestAlgorithm`` SEQUENCE.
+
+ :attention: the early standard (RFC2313) stated that ``DigestInfo``
+ had to be BER-encoded. This means that old signatures
+ might have length tags in indefinite form, which
+ is not supported in DER. Such encoding cannot be
+ reproduced by this function.
+
+ :Return: An ``emLen`` byte long string that encodes the hash.
+ """
+
+ # First, build the ASN.1 DER object DigestInfo:
+ #
+ # DigestInfo ::= SEQUENCE {
+ # digestAlgorithm AlgorithmIdentifier,
+ # digest OCTET STRING
+ # }
+ #
+ # where digestAlgorithm identifies the hash function and shall be an
+ # algorithm ID with an OID in the set PKCS1-v1-5DigestAlgorithms.
+ #
+ # PKCS1-v1-5DigestAlgorithms ALGORITHM-IDENTIFIER ::= {
+ # { OID id-md2 PARAMETERS NULL }|
+ # { OID id-md5 PARAMETERS NULL }|
+ # { OID id-sha1 PARAMETERS NULL }|
+ # { OID id-sha256 PARAMETERS NULL }|
+ # { OID id-sha384 PARAMETERS NULL }|
+ # { OID id-sha512 PARAMETERS NULL }
+ # }
+ #
+ # Appendix B.1 also says that for SHA-1/-2 algorithms, the parameters
+ # should be omitted. They may be present, but when they are, they shall
+ # have NULL value.
+
+ digestAlgo = DerSequence([ DerObjectId(msg_hash.oid).encode() ])
+
+ if with_hash_parameters:
+ digestAlgo.append(DerNull().encode())
+
+ digest = DerOctetString(msg_hash.digest())
+ digestInfo = DerSequence([
+ digestAlgo.encode(),
+ digest.encode()
+ ]).encode()
+
+ # We need at least 11 bytes for the remaining data: 3 fixed bytes and
+ # at least 8 bytes of padding).
+ if emLen bytes: ...
+
+class PKCS115_SigScheme:
+ def __init__(self, rsa_key: RsaKey) -> None: ...
+ def can_sign(self) -> bool: ...
+ def sign(self, msg_hash: Hash) -> bytes: ...
+ def verify(self, msg_hash: Hash, signature: bytes) -> None: ...
+
+def _EMSA_PKCS1_V1_5_ENCODE(msg_hash: Hash, emLen: int, with_hash_parameters: Optional[bool]=True) -> bytes: ...
+
+def new(rsa_key: RsaKey) -> PKCS115_SigScheme: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/pss.py b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/pss.py
new file mode 100644
index 00000000..f2295df6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/pss.py
@@ -0,0 +1,387 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.Util.py3compat import bchr, bord, iter_range
+import Crypto.Util.number
+from Crypto.Util.number import (ceil_div,
+ long_to_bytes,
+ bytes_to_long
+ )
+from Crypto.Util.strxor import strxor
+from Crypto import Random
+
+
+class PSS_SigScheme:
+ """A signature object for ``RSASSA-PSS``.
+ Do not instantiate directly.
+ Use :func:`Crypto.Signature.pss.new`.
+ """
+
+ def __init__(self, key, mgfunc, saltLen, randfunc):
+ """Initialize this PKCS#1 PSS signature scheme object.
+
+ :Parameters:
+ key : an RSA key object
+ If a private half is given, both signature and
+ verification are possible.
+ If a public half is given, only verification is possible.
+ 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.
+ saltLen : integer
+ Length of the salt, in bytes.
+ randfunc : callable
+ A function that returns random bytes.
+ """
+
+ self._key = key
+ self._saltLen = saltLen
+ self._mgfunc = mgfunc
+ self._randfunc = randfunc
+
+ def can_sign(self):
+ """Return ``True`` if this object can be used to sign messages."""
+ return self._key.has_private()
+
+ def sign(self, msg_hash):
+ """Create the PKCS#1 PSS signature of a message.
+
+ This function is also called ``RSASSA-PSS-SIGN`` and
+ it is specified in
+ `section 8.1.1 of RFC8017 `_.
+
+ :parameter msg_hash:
+ This is an object from the :mod:`Crypto.Hash` package.
+ It has been used to digest the message to sign.
+ :type msg_hash: hash object
+
+ :return: the signature encoded as a *byte string*.
+ :raise ValueError: if the RSA key is not long enough for the given hash algorithm.
+ :raise TypeError: if the RSA key has no private half.
+ """
+
+ # Set defaults for salt length and mask generation function
+ if self._saltLen is None:
+ sLen = msg_hash.digest_size
+ else:
+ sLen = self._saltLen
+
+ if self._mgfunc is None:
+ mgf = lambda x, y: MGF1(x, y, msg_hash)
+ else:
+ mgf = self._mgfunc
+
+ modBits = Crypto.Util.number.size(self._key.n)
+
+ # See 8.1.1 in RFC3447
+ k = ceil_div(modBits, 8) # k is length in bytes of the modulus
+ # Step 1
+ em = _EMSA_PSS_ENCODE(msg_hash, modBits-1, self._randfunc, mgf, sLen)
+ # Step 2a (OS2IP)
+ em_int = bytes_to_long(em)
+ # Step 2b (RSASP1) and Step 2c (I2OSP)
+ signature = self._key._decrypt_to_bytes(em_int)
+ # Verify no faults occurred
+ if em_int != pow(bytes_to_long(signature), self._key.e, self._key.n):
+ raise ValueError("Fault detected in RSA private key operation")
+ return signature
+
+ def verify(self, msg_hash, signature):
+ """Check if the PKCS#1 PSS signature over a message is valid.
+
+ This function is also called ``RSASSA-PSS-VERIFY`` and
+ it is specified in
+ `section 8.1.2 of RFC8037 `_.
+
+ :parameter msg_hash:
+ The hash that was carried out over the message. This is an object
+ belonging to the :mod:`Crypto.Hash` module.
+ :type parameter: hash object
+
+ :parameter signature:
+ The signature that needs to be validated.
+ :type signature: bytes
+
+ :raise ValueError: if the signature is not valid.
+ """
+
+ # Set defaults for salt length and mask generation function
+ if self._saltLen is None:
+ sLen = msg_hash.digest_size
+ else:
+ sLen = self._saltLen
+ if self._mgfunc:
+ mgf = self._mgfunc
+ else:
+ mgf = lambda x, y: MGF1(x, y, msg_hash)
+
+ modBits = Crypto.Util.number.size(self._key.n)
+
+ # See 8.1.2 in RFC3447
+ k = ceil_div(modBits, 8) # Convert from bits to bytes
+ # Step 1
+ if len(signature) != k:
+ raise ValueError("Incorrect signature")
+ # Step 2a (O2SIP)
+ signature_int = bytes_to_long(signature)
+ # Step 2b (RSAVP1)
+ em_int = self._key._encrypt(signature_int)
+ # Step 2c (I2OSP)
+ emLen = ceil_div(modBits - 1, 8)
+ em = long_to_bytes(em_int, emLen)
+ # Step 3/4
+ _EMSA_PSS_VERIFY(msg_hash, em, modBits-1, mgf, sLen)
+
+
+def MGF1(mgfSeed, maskLen, hash_gen):
+ """Mask Generation Function, described in `B.2.1 of RFC8017
+ `_.
+
+ :param mfgSeed:
+ seed from which the mask is generated
+ :type mfgSeed: byte string
+
+ :param maskLen:
+ intended length in bytes of the mask
+ :type maskLen: integer
+
+ :param hash_gen:
+ A module or a hash object from :mod:`Crypto.Hash`
+ :type hash_object:
+
+ :return: the mask, as a *byte string*
+ """
+
+ T = b""
+ for counter in iter_range(ceil_div(maskLen, hash_gen.digest_size)):
+ c = long_to_bytes(counter, 4)
+ hobj = hash_gen.new()
+ hobj.update(mgfSeed + c)
+ T = T + hobj.digest()
+ assert(len(T) >= maskLen)
+ return T[:maskLen]
+
+
+def _EMSA_PSS_ENCODE(mhash, emBits, randFunc, mgf, sLen):
+ r"""
+ Implement the ``EMSA-PSS-ENCODE`` function, as defined
+ in PKCS#1 v2.1 (RFC3447, 9.1.1).
+
+ The original ``EMSA-PSS-ENCODE`` actually accepts the message ``M``
+ as input, and hash it internally. Here, we expect that the message
+ has already been hashed instead.
+
+ :Parameters:
+ mhash : hash object
+ The hash object that holds the digest of the message being signed.
+ emBits : int
+ Maximum length of the final encoding, in bits.
+ randFunc : callable
+ An RNG function that accepts as only parameter an int, and returns
+ a string of random bytes, to be used as salt.
+ mgf : 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.
+ sLen : int
+ Length of the salt, in bytes.
+
+ :Return: An ``emLen`` byte long string that encodes the hash
+ (with ``emLen = \ceil(emBits/8)``).
+
+ :Raise ValueError:
+ When digest or salt length are too big.
+ """
+
+ emLen = ceil_div(emBits, 8)
+
+ # Bitmask of digits that fill up
+ lmask = 0
+ for i in iter_range(8*emLen-emBits):
+ lmask = lmask >> 1 | 0x80
+
+ # Step 1 and 2 have been already done
+ # Step 3
+ if emLen < mhash.digest_size+sLen+2:
+ raise ValueError("Digest or salt length are too long"
+ " for given key size.")
+ # Step 4
+ salt = randFunc(sLen)
+ # Step 5
+ m_prime = bchr(0)*8 + mhash.digest() + salt
+ # Step 6
+ h = mhash.new()
+ h.update(m_prime)
+ # Step 7
+ ps = bchr(0)*(emLen-sLen-mhash.digest_size-2)
+ # Step 8
+ db = ps + bchr(1) + salt
+ # Step 9
+ dbMask = mgf(h.digest(), emLen-mhash.digest_size-1)
+ # Step 10
+ maskedDB = strxor(db, dbMask)
+ # Step 11
+ maskedDB = bchr(bord(maskedDB[0]) & ~lmask) + maskedDB[1:]
+ # Step 12
+ em = maskedDB + h.digest() + bchr(0xBC)
+ return em
+
+
+def _EMSA_PSS_VERIFY(mhash, em, emBits, mgf, sLen):
+ """
+ Implement the ``EMSA-PSS-VERIFY`` function, as defined
+ in PKCS#1 v2.1 (RFC3447, 9.1.2).
+
+ ``EMSA-PSS-VERIFY`` actually accepts the message ``M`` as input,
+ and hash it internally. Here, we expect that the message has already
+ been hashed instead.
+
+ :Parameters:
+ mhash : hash object
+ The hash object that holds the digest of the message to be verified.
+ em : string
+ The signature to verify, therefore proving that the sender really
+ signed the message that was received.
+ emBits : int
+ Length of the final encoding (em), in bits.
+ mgf : 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.
+ sLen : int
+ Length of the salt, in bytes.
+
+ :Raise ValueError:
+ When the encoding is inconsistent, or the digest or salt lengths
+ are too big.
+ """
+
+ emLen = ceil_div(emBits, 8)
+
+ # Bitmask of digits that fill up
+ lmask = 0
+ for i in iter_range(8*emLen-emBits):
+ lmask = lmask >> 1 | 0x80
+
+ # Step 1 and 2 have been already done
+ # Step 3
+ if emLen < mhash.digest_size+sLen+2:
+ raise ValueError("Incorrect signature")
+ # Step 4
+ if ord(em[-1:]) != 0xBC:
+ raise ValueError("Incorrect signature")
+ # Step 5
+ maskedDB = em[:emLen-mhash.digest_size-1]
+ h = em[emLen-mhash.digest_size-1:-1]
+ # Step 6
+ if lmask & bord(em[0]):
+ raise ValueError("Incorrect signature")
+ # Step 7
+ dbMask = mgf(h, emLen-mhash.digest_size-1)
+ # Step 8
+ db = strxor(maskedDB, dbMask)
+ # Step 9
+ db = bchr(bord(db[0]) & ~lmask) + db[1:]
+ # Step 10
+ if not db.startswith(bchr(0)*(emLen-mhash.digest_size-sLen-2) + bchr(1)):
+ raise ValueError("Incorrect signature")
+ # Step 11
+ if sLen > 0:
+ salt = db[-sLen:]
+ else:
+ salt = b""
+ # Step 12
+ m_prime = bchr(0)*8 + mhash.digest() + salt
+ # Step 13
+ hobj = mhash.new()
+ hobj.update(m_prime)
+ hp = hobj.digest()
+ # Step 14
+ if h != hp:
+ raise ValueError("Incorrect signature")
+
+
+def new(rsa_key, **kwargs):
+ """Create an object for making or verifying PKCS#1 PSS signatures.
+
+ :parameter rsa_key:
+ The RSA key to use for signing or verifying the message.
+ This is a :class:`Crypto.PublicKey.RSA` object.
+ Signing is only possible when ``rsa_key`` is a **private** RSA key.
+ :type rsa_key: RSA object
+
+ :Keyword Arguments:
+
+ * *mask_func* (``callable``) --
+ A function that returns the mask (as `bytes`).
+ It must accept two parameters: a seed (as `bytes`)
+ and the length of the data to return.
+
+ If not specified, it will be the function :func:`MGF1` defined in
+ `RFC8017 `_ and
+ combined with the same hash algorithm applied to the
+ message to sign or verify.
+
+ If you want to use a different function, for instance still :func:`MGF1`
+ but together with another hash, you can do::
+
+ from Crypto.Hash import SHA256
+ from Crypto.Signature.pss import MGF1
+ mgf = lambda x, y: MGF1(x, y, SHA256)
+
+ * *salt_bytes* (``integer``) --
+ Length of the salt, in bytes.
+ It is a value between 0 and ``emLen - hLen - 2``, where ``emLen``
+ is the size of the RSA modulus and ``hLen`` is the size of the digest
+ applied to the message to sign or verify.
+
+ The salt is generated internally, you don't need to provide it.
+
+ If not specified, the salt length will be ``hLen``.
+ If it is zero, the signature scheme becomes deterministic.
+
+ Note that in some implementations such as OpenSSL the default
+ salt length is ``emLen - hLen - 2`` (even though it is not more
+ secure than ``hLen``).
+
+ * *rand_func* (``callable``) --
+ A function that returns random ``bytes``, of the desired length.
+ The default is :func:`Crypto.Random.get_random_bytes`.
+
+ :return: a :class:`PSS_SigScheme` signature object
+ """
+
+ mask_func = kwargs.pop("mask_func", None)
+ salt_len = kwargs.pop("salt_bytes", None)
+ rand_func = kwargs.pop("rand_func", None)
+ if rand_func is None:
+ rand_func = Random.get_random_bytes
+ if kwargs:
+ raise ValueError("Unknown keywords: " + str(kwargs.keys()))
+ return PSS_SigScheme(rsa_key, mask_func, salt_len, rand_func)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Signature/pss.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/pss.pyi
new file mode 100644
index 00000000..d4088e16
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Signature/pss.pyi
@@ -0,0 +1,30 @@
+from typing import Union, Callable, Optional
+from typing_extensions import Protocol
+
+from Crypto.PublicKey.RSA import RsaKey
+
+
+class Hash(Protocol):
+ def digest(self) -> bytes: ...
+ def update(self, bytes) -> None: ...
+
+
+class HashModule(Protocol):
+ @staticmethod
+ def new(data: Optional[bytes]) -> Hash: ...
+
+
+MaskFunction = Callable[[bytes, int, Union[Hash, HashModule]], bytes]
+RndFunction = Callable[[int], bytes]
+
+class PSS_SigScheme:
+ def __init__(self, key: RsaKey, mgfunc: MaskFunction, saltLen: int, randfunc: RndFunction) -> None: ...
+ def can_sign(self) -> bool: ...
+ def sign(self, msg_hash: Hash) -> bytes: ...
+ def verify(self, msg_hash: Hash, signature: bytes) -> None: ...
+
+
+MGF1 : MaskFunction
+def _EMSA_PSS_ENCODE(mhash: Hash, emBits: int, randFunc: RndFunction, mgf:MaskFunction, sLen: int) -> str: ...
+def _EMSA_PSS_VERIFY(mhash: Hash, em: str, emBits: int, mgf: MaskFunction, sLen: int) -> None: ...
+def new(rsa_key: RsaKey, **kwargs: Union[MaskFunction, RndFunction, int]) -> PSS_SigScheme: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/Counter.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/Counter.py
new file mode 100644
index 00000000..e3bdcbec
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/Counter.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+#
+# Util/Counter.py : Fast counter for use with CTR-mode ciphers
+#
+# Written in 2008 by Dwayne C. Litzenberger
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+def new(nbits, prefix=b"", suffix=b"", initial_value=1, little_endian=False, allow_wraparound=False):
+ """Create a stateful counter block function suitable for CTR encryption modes.
+
+ Each call to the function returns the next counter block.
+ Each counter block is made up by three parts:
+
+ +------+--------------+-------+
+ |prefix| counter value|postfix|
+ +------+--------------+-------+
+
+ The counter value is incremented by 1 at each call.
+
+ Args:
+ nbits (integer):
+ Length of the desired counter value, in bits. It must be a multiple of 8.
+ prefix (byte string):
+ The constant prefix of the counter block. By default, no prefix is
+ used.
+ suffix (byte string):
+ The constant postfix of the counter block. By default, no suffix is
+ used.
+ initial_value (integer):
+ The initial value of the counter. Default value is 1.
+ Its length in bits must not exceed the argument ``nbits``.
+ little_endian (boolean):
+ If ``True``, the counter number will be encoded in little endian format.
+ If ``False`` (default), in big endian format.
+ allow_wraparound (boolean):
+ This parameter is ignored.
+ An ``OverflowError`` exception is always raised when the counter wraps
+ around to zero.
+ Returns:
+ An object that can be passed with the :data:`counter` parameter to a CTR mode
+ cipher.
+
+ It must hold that *len(prefix) + nbits//8 + len(suffix)* matches the
+ block size of the underlying block cipher.
+ """
+
+ if (nbits % 8) != 0:
+ raise ValueError("'nbits' must be a multiple of 8")
+
+ iv_bl = initial_value.bit_length()
+ if iv_bl > nbits:
+ raise ValueError("Initial value takes %d bits but it is longer than "
+ "the counter (%d bits)" %
+ (iv_bl, nbits))
+
+ # Ignore wraparound
+ return {"counter_len": nbits // 8,
+ "prefix": prefix,
+ "suffix": suffix,
+ "initial_value": initial_value,
+ "little_endian": little_endian
+ }
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/Counter.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Util/Counter.pyi
new file mode 100644
index 00000000..fa2ffdd8
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/Counter.pyi
@@ -0,0 +1,5 @@
+from typing import Optional, Union, Dict
+
+def new(nbits: int, prefix: Optional[bytes]=..., suffix: Optional[bytes]=..., initial_value: Optional[int]=1,
+ little_endian: Optional[bool]=False, allow_wraparound: Optional[bool]=False) -> \
+ Dict[str, Union[int, bytes, bool]]: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/Padding.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/Padding.py
new file mode 100644
index 00000000..d5b1cca0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/Padding.py
@@ -0,0 +1,119 @@
+#
+# Util/Padding.py : Functions to manage padding
+#
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.
+# ===================================================================
+
+__all__ = [ 'pad', 'unpad' ]
+
+from Crypto.Util.py3compat import *
+
+
+def pad(data_to_pad, block_size, style='pkcs7'):
+ """Apply standard padding.
+
+ Args:
+ data_to_pad (byte string):
+ The data that needs to be padded.
+ block_size (integer):
+ The block boundary to use for padding. The output length is guaranteed
+ to be a multiple of :data:`block_size`.
+ style (string):
+ Padding algorithm. It can be *'pkcs7'* (default), *'iso7816'* or *'x923'*.
+
+ Return:
+ byte string : the original data with the appropriate padding added at the end.
+ """
+
+ padding_len = block_size - len(data_to_pad) % block_size
+
+ if style == 'pkcs7':
+ padding = bchr(padding_len) * padding_len
+ elif style == 'x923':
+ padding = bchr(0)*(padding_len-1) + bchr(padding_len)
+ elif style == 'iso7816':
+ padding = bchr(128) + bchr(0) * (padding_len-1)
+ else:
+ raise ValueError("Unknown padding style")
+
+ return data_to_pad + padding
+
+
+def unpad(padded_data, block_size, style='pkcs7'):
+ """Remove standard padding.
+
+ Args:
+ padded_data (byte string):
+ A piece of data with padding that needs to be stripped.
+ block_size (integer):
+ The block boundary to use for padding. The input length
+ must be a multiple of :data:`block_size`.
+ style (string):
+ Padding algorithm. It can be *'pkcs7'* (default), *'iso7816'* or *'x923'*.
+ Return:
+ byte string : data without padding.
+ Raises:
+ ValueError: if the padding is incorrect.
+ """
+
+ pdata_len = len(padded_data)
+
+ if pdata_len == 0:
+ raise ValueError("Zero-length input cannot be unpadded")
+
+ if pdata_len % block_size:
+ raise ValueError("Input data is not padded")
+
+ if style in ('pkcs7', 'x923'):
+ padding_len = bord(padded_data[-1])
+
+ if padding_len < 1 or padding_len > min(block_size, pdata_len):
+ raise ValueError("Padding is incorrect.")
+
+ if style == 'pkcs7':
+ if padded_data[-padding_len:] != bchr(padding_len)*padding_len:
+ raise ValueError("PKCS#7 padding is incorrect.")
+ else:
+ if padded_data[-padding_len:-1] != bchr(0)*(padding_len-1):
+ raise ValueError("ANSI X.923 padding is incorrect.")
+
+ elif style == 'iso7816':
+ padding_len = pdata_len - padded_data.rfind(bchr(128))
+
+ if padding_len < 1 or padding_len > min(block_size, pdata_len):
+ raise ValueError("Padding is incorrect.")
+
+ if padding_len > 1 and padded_data[1-padding_len:] != bchr(0)*(padding_len-1):
+ raise ValueError("ISO 7816-4 padding is incorrect.")
+ else:
+ raise ValueError("Unknown padding style")
+
+ return padded_data[:-padding_len]
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/Padding.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Util/Padding.pyi
new file mode 100644
index 00000000..4d8d30df
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/Padding.pyi
@@ -0,0 +1,6 @@
+from typing import Optional
+
+__all__ = [ 'pad', 'unpad' ]
+
+def pad(data_to_pad: bytes, block_size: int, style: Optional[str]='pkcs7') -> bytes: ...
+def unpad(padded_data: bytes, block_size: int, style: Optional[str]='pkcs7') -> bytes: ...
\ No newline at end of file
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/RFC1751.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/RFC1751.py
new file mode 100644
index 00000000..9ed52d2c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/RFC1751.py
@@ -0,0 +1,386 @@
+# rfc1751.py : Converts between 128-bit strings and a human-readable
+# sequence of words, as defined in RFC1751: "A Convention for
+# Human-Readable 128-bit Keys", by Daniel L. McDonald.
+#
+# Part of the Python Cryptography Toolkit
+#
+# Written by Andrew M. Kuchling and others
+#
+# ===================================================================
+# 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 __future__ import print_function
+
+import binascii
+
+from Crypto.Util.py3compat import bord, bchr
+
+binary = {0: '0000', 1: '0001', 2: '0010', 3: '0011', 4: '0100', 5: '0101',
+ 6: '0110', 7: '0111', 8: '1000', 9: '1001', 10: '1010', 11: '1011',
+ 12: '1100', 13: '1101', 14: '1110', 15: '1111'}
+
+
+def _key2bin(s):
+ "Convert a key into a string of binary digits"
+ kl = map(lambda x: bord(x), s)
+ kl = map(lambda x: binary[x >> 4] + binary[x & 15], kl)
+ return ''.join(kl)
+
+
+def _extract(key, start, length):
+ """Extract a bitstring(2.x)/bytestring(2.x) from a string of binary digits, and return its
+ numeric value."""
+
+ result = 0
+ for y in key[start:start+length]:
+ result = result * 2 + ord(y) - 48
+ return result
+
+
+def key_to_english(key):
+ """Transform an arbitrary key into a string containing English words.
+
+ Example::
+
+ >>> from Crypto.Util.RFC1751 import key_to_english
+ >>> key_to_english(b'66666666')
+ 'RAM LOIS GOAD CREW CARE HIT'
+
+ Args:
+ key (byte string):
+ The key to convert. Its length must be a multiple of 8.
+ Return:
+ A string of English words.
+ """
+
+ if len(key) % 8 != 0:
+ raise ValueError('The length of the key must be a multiple of 8.')
+
+ english = ''
+ for index in range(0, len(key), 8): # Loop over 8-byte subkeys
+ subkey = key[index:index + 8]
+ # Compute the parity of the key
+ skbin = _key2bin(subkey)
+ p = 0
+ for i in range(0, 64, 2):
+ p = p + _extract(skbin, i, 2)
+ # Append parity bits to the subkey
+ skbin = _key2bin(subkey + bchr((p << 6) & 255))
+ for i in range(0, 64, 11):
+ english = english + wordlist[_extract(skbin, i, 11)] + ' '
+
+ return english.strip()
+
+
+def english_to_key(s):
+ """Transform a string into a corresponding key.
+
+ Example::
+
+ >>> from Crypto.Util.RFC1751 import english_to_key
+ >>> english_to_key('RAM LOIS GOAD CREW CARE HIT')
+ b'66666666'
+
+ Args:
+ s (string): the string with the words separated by whitespace;
+ the number of words must be a multiple of 6.
+ Return:
+ A byte string.
+ """
+
+ L = s.upper().split()
+ key = b''
+ for index in range(0, len(L), 6):
+ sublist = L[index:index + 6]
+ char = 9 * [0]
+ bits = 0
+ for i in sublist:
+ index = wordlist.index(i)
+ shift = (8 - (bits + 11) % 8) % 8
+ y = index << shift
+ cl, cc, cr = (y >> 16), (y >> 8) & 0xff, y & 0xff
+ if (shift > 5):
+ char[bits >> 3] = char[bits >> 3] | cl
+ char[(bits >> 3) + 1] = char[(bits >> 3) + 1] | cc
+ char[(bits >> 3) + 2] = char[(bits >> 3) + 2] | cr
+ elif shift > -3:
+ char[bits >> 3] = char[bits >> 3] | cc
+ char[(bits >> 3) + 1] = char[(bits >> 3) + 1] | cr
+ else:
+ char[bits >> 3] = char[bits >> 3] | cr
+ bits = bits + 11
+
+ subkey = b''
+ for y in char:
+ subkey = subkey + bchr(y)
+
+ # Check the parity of the resulting key
+ skbin = _key2bin(subkey)
+ p = 0
+ for i in range(0, 64, 2):
+ p = p + _extract(skbin, i, 2)
+ if (p & 3) != _extract(skbin, 64, 2):
+ raise ValueError("Parity error in resulting key")
+ key = key + subkey[0:8]
+ return key
+
+
+wordlist = [
+ "A", "ABE", "ACE", "ACT", "AD", "ADA", "ADD",
+ "AGO", "AID", "AIM", "AIR", "ALL", "ALP", "AM", "AMY", "AN", "ANA",
+ "AND", "ANN", "ANT", "ANY", "APE", "APS", "APT", "ARC", "ARE", "ARK",
+ "ARM", "ART", "AS", "ASH", "ASK", "AT", "ATE", "AUG", "AUK", "AVE",
+ "AWE", "AWK", "AWL", "AWN", "AX", "AYE", "BAD", "BAG", "BAH", "BAM",
+ "BAN", "BAR", "BAT", "BAY", "BE", "BED", "BEE", "BEG", "BEN", "BET",
+ "BEY", "BIB", "BID", "BIG", "BIN", "BIT", "BOB", "BOG", "BON", "BOO",
+ "BOP", "BOW", "BOY", "BUB", "BUD", "BUG", "BUM", "BUN", "BUS", "BUT",
+ "BUY", "BY", "BYE", "CAB", "CAL", "CAM", "CAN", "CAP", "CAR", "CAT",
+ "CAW", "COD", "COG", "COL", "CON", "COO", "COP", "COT", "COW", "COY",
+ "CRY", "CUB", "CUE", "CUP", "CUR", "CUT", "DAB", "DAD", "DAM", "DAN",
+ "DAR", "DAY", "DEE", "DEL", "DEN", "DES", "DEW", "DID", "DIE", "DIG",
+ "DIN", "DIP", "DO", "DOE", "DOG", "DON", "DOT", "DOW", "DRY", "DUB",
+ "DUD", "DUE", "DUG", "DUN", "EAR", "EAT", "ED", "EEL", "EGG", "EGO",
+ "ELI", "ELK", "ELM", "ELY", "EM", "END", "EST", "ETC", "EVA", "EVE",
+ "EWE", "EYE", "FAD", "FAN", "FAR", "FAT", "FAY", "FED", "FEE", "FEW",
+ "FIB", "FIG", "FIN", "FIR", "FIT", "FLO", "FLY", "FOE", "FOG", "FOR",
+ "FRY", "FUM", "FUN", "FUR", "GAB", "GAD", "GAG", "GAL", "GAM", "GAP",
+ "GAS", "GAY", "GEE", "GEL", "GEM", "GET", "GIG", "GIL", "GIN", "GO",
+ "GOT", "GUM", "GUN", "GUS", "GUT", "GUY", "GYM", "GYP", "HA", "HAD",
+ "HAL", "HAM", "HAN", "HAP", "HAS", "HAT", "HAW", "HAY", "HE", "HEM",
+ "HEN", "HER", "HEW", "HEY", "HI", "HID", "HIM", "HIP", "HIS", "HIT",
+ "HO", "HOB", "HOC", "HOE", "HOG", "HOP", "HOT", "HOW", "HUB", "HUE",
+ "HUG", "HUH", "HUM", "HUT", "I", "ICY", "IDA", "IF", "IKE", "ILL",
+ "INK", "INN", "IO", "ION", "IQ", "IRA", "IRE", "IRK", "IS", "IT",
+ "ITS", "IVY", "JAB", "JAG", "JAM", "JAN", "JAR", "JAW", "JAY", "JET",
+ "JIG", "JIM", "JO", "JOB", "JOE", "JOG", "JOT", "JOY", "JUG", "JUT",
+ "KAY", "KEG", "KEN", "KEY", "KID", "KIM", "KIN", "KIT", "LA", "LAB",
+ "LAC", "LAD", "LAG", "LAM", "LAP", "LAW", "LAY", "LEA", "LED", "LEE",
+ "LEG", "LEN", "LEO", "LET", "LEW", "LID", "LIE", "LIN", "LIP", "LIT",
+ "LO", "LOB", "LOG", "LOP", "LOS", "LOT", "LOU", "LOW", "LOY", "LUG",
+ "LYE", "MA", "MAC", "MAD", "MAE", "MAN", "MAO", "MAP", "MAT", "MAW",
+ "MAY", "ME", "MEG", "MEL", "MEN", "MET", "MEW", "MID", "MIN", "MIT",
+ "MOB", "MOD", "MOE", "MOO", "MOP", "MOS", "MOT", "MOW", "MUD", "MUG",
+ "MUM", "MY", "NAB", "NAG", "NAN", "NAP", "NAT", "NAY", "NE", "NED",
+ "NEE", "NET", "NEW", "NIB", "NIL", "NIP", "NIT", "NO", "NOB", "NOD",
+ "NON", "NOR", "NOT", "NOV", "NOW", "NU", "NUN", "NUT", "O", "OAF",
+ "OAK", "OAR", "OAT", "ODD", "ODE", "OF", "OFF", "OFT", "OH", "OIL",
+ "OK", "OLD", "ON", "ONE", "OR", "ORB", "ORE", "ORR", "OS", "OTT",
+ "OUR", "OUT", "OVA", "OW", "OWE", "OWL", "OWN", "OX", "PA", "PAD",
+ "PAL", "PAM", "PAN", "PAP", "PAR", "PAT", "PAW", "PAY", "PEA", "PEG",
+ "PEN", "PEP", "PER", "PET", "PEW", "PHI", "PI", "PIE", "PIN", "PIT",
+ "PLY", "PO", "POD", "POE", "POP", "POT", "POW", "PRO", "PRY", "PUB",
+ "PUG", "PUN", "PUP", "PUT", "QUO", "RAG", "RAM", "RAN", "RAP", "RAT",
+ "RAW", "RAY", "REB", "RED", "REP", "RET", "RIB", "RID", "RIG", "RIM",
+ "RIO", "RIP", "ROB", "ROD", "ROE", "RON", "ROT", "ROW", "ROY", "RUB",
+ "RUE", "RUG", "RUM", "RUN", "RYE", "SAC", "SAD", "SAG", "SAL", "SAM",
+ "SAN", "SAP", "SAT", "SAW", "SAY", "SEA", "SEC", "SEE", "SEN", "SET",
+ "SEW", "SHE", "SHY", "SIN", "SIP", "SIR", "SIS", "SIT", "SKI", "SKY",
+ "SLY", "SO", "SOB", "SOD", "SON", "SOP", "SOW", "SOY", "SPA", "SPY",
+ "SUB", "SUD", "SUE", "SUM", "SUN", "SUP", "TAB", "TAD", "TAG", "TAN",
+ "TAP", "TAR", "TEA", "TED", "TEE", "TEN", "THE", "THY", "TIC", "TIE",
+ "TIM", "TIN", "TIP", "TO", "TOE", "TOG", "TOM", "TON", "TOO", "TOP",
+ "TOW", "TOY", "TRY", "TUB", "TUG", "TUM", "TUN", "TWO", "UN", "UP",
+ "US", "USE", "VAN", "VAT", "VET", "VIE", "WAD", "WAG", "WAR", "WAS",
+ "WAY", "WE", "WEB", "WED", "WEE", "WET", "WHO", "WHY", "WIN", "WIT",
+ "WOK", "WON", "WOO", "WOW", "WRY", "WU", "YAM", "YAP", "YAW", "YE",
+ "YEA", "YES", "YET", "YOU", "ABED", "ABEL", "ABET", "ABLE", "ABUT",
+ "ACHE", "ACID", "ACME", "ACRE", "ACTA", "ACTS", "ADAM", "ADDS",
+ "ADEN", "AFAR", "AFRO", "AGEE", "AHEM", "AHOY", "AIDA", "AIDE",
+ "AIDS", "AIRY", "AJAR", "AKIN", "ALAN", "ALEC", "ALGA", "ALIA",
+ "ALLY", "ALMA", "ALOE", "ALSO", "ALTO", "ALUM", "ALVA", "AMEN",
+ "AMES", "AMID", "AMMO", "AMOK", "AMOS", "AMRA", "ANDY", "ANEW",
+ "ANNA", "ANNE", "ANTE", "ANTI", "AQUA", "ARAB", "ARCH", "AREA",
+ "ARGO", "ARID", "ARMY", "ARTS", "ARTY", "ASIA", "ASKS", "ATOM",
+ "AUNT", "AURA", "AUTO", "AVER", "AVID", "AVIS", "AVON", "AVOW",
+ "AWAY", "AWRY", "BABE", "BABY", "BACH", "BACK", "BADE", "BAIL",
+ "BAIT", "BAKE", "BALD", "BALE", "BALI", "BALK", "BALL", "BALM",
+ "BAND", "BANE", "BANG", "BANK", "BARB", "BARD", "BARE", "BARK",
+ "BARN", "BARR", "BASE", "BASH", "BASK", "BASS", "BATE", "BATH",
+ "BAWD", "BAWL", "BEAD", "BEAK", "BEAM", "BEAN", "BEAR", "BEAT",
+ "BEAU", "BECK", "BEEF", "BEEN", "BEER",
+ "BEET", "BELA", "BELL", "BELT", "BEND", "BENT", "BERG", "BERN",
+ "BERT", "BESS", "BEST", "BETA", "BETH", "BHOY", "BIAS", "BIDE",
+ "BIEN", "BILE", "BILK", "BILL", "BIND", "BING", "BIRD", "BITE",
+ "BITS", "BLAB", "BLAT", "BLED", "BLEW", "BLOB", "BLOC", "BLOT",
+ "BLOW", "BLUE", "BLUM", "BLUR", "BOAR", "BOAT", "BOCA", "BOCK",
+ "BODE", "BODY", "BOGY", "BOHR", "BOIL", "BOLD", "BOLO", "BOLT",
+ "BOMB", "BONA", "BOND", "BONE", "BONG", "BONN", "BONY", "BOOK",
+ "BOOM", "BOON", "BOOT", "BORE", "BORG", "BORN", "BOSE", "BOSS",
+ "BOTH", "BOUT", "BOWL", "BOYD", "BRAD", "BRAE", "BRAG", "BRAN",
+ "BRAY", "BRED", "BREW", "BRIG", "BRIM", "BROW", "BUCK", "BUDD",
+ "BUFF", "BULB", "BULK", "BULL", "BUNK", "BUNT", "BUOY", "BURG",
+ "BURL", "BURN", "BURR", "BURT", "BURY", "BUSH", "BUSS", "BUST",
+ "BUSY", "BYTE", "CADY", "CAFE", "CAGE", "CAIN", "CAKE", "CALF",
+ "CALL", "CALM", "CAME", "CANE", "CANT", "CARD", "CARE", "CARL",
+ "CARR", "CART", "CASE", "CASH", "CASK", "CAST", "CAVE", "CEIL",
+ "CELL", "CENT", "CERN", "CHAD", "CHAR", "CHAT", "CHAW", "CHEF",
+ "CHEN", "CHEW", "CHIC", "CHIN", "CHOU", "CHOW", "CHUB", "CHUG",
+ "CHUM", "CITE", "CITY", "CLAD", "CLAM", "CLAN", "CLAW", "CLAY",
+ "CLOD", "CLOG", "CLOT", "CLUB", "CLUE", "COAL", "COAT", "COCA",
+ "COCK", "COCO", "CODA", "CODE", "CODY", "COED", "COIL", "COIN",
+ "COKE", "COLA", "COLD", "COLT", "COMA", "COMB", "COME", "COOK",
+ "COOL", "COON", "COOT", "CORD", "CORE", "CORK", "CORN", "COST",
+ "COVE", "COWL", "CRAB", "CRAG", "CRAM", "CRAY", "CREW", "CRIB",
+ "CROW", "CRUD", "CUBA", "CUBE", "CUFF", "CULL", "CULT", "CUNY",
+ "CURB", "CURD", "CURE", "CURL", "CURT", "CUTS", "DADE", "DALE",
+ "DAME", "DANA", "DANE", "DANG", "DANK", "DARE", "DARK", "DARN",
+ "DART", "DASH", "DATA", "DATE", "DAVE", "DAVY", "DAWN", "DAYS",
+ "DEAD", "DEAF", "DEAL", "DEAN", "DEAR", "DEBT", "DECK", "DEED",
+ "DEEM", "DEER", "DEFT", "DEFY", "DELL", "DENT", "DENY", "DESK",
+ "DIAL", "DICE", "DIED", "DIET", "DIME", "DINE", "DING", "DINT",
+ "DIRE", "DIRT", "DISC", "DISH", "DISK", "DIVE", "DOCK", "DOES",
+ "DOLE", "DOLL", "DOLT", "DOME", "DONE", "DOOM", "DOOR", "DORA",
+ "DOSE", "DOTE", "DOUG", "DOUR", "DOVE", "DOWN", "DRAB", "DRAG",
+ "DRAM", "DRAW", "DREW", "DRUB", "DRUG", "DRUM", "DUAL", "DUCK",
+ "DUCT", "DUEL", "DUET", "DUKE", "DULL", "DUMB", "DUNE", "DUNK",
+ "DUSK", "DUST", "DUTY", "EACH", "EARL", "EARN", "EASE", "EAST",
+ "EASY", "EBEN", "ECHO", "EDDY", "EDEN", "EDGE", "EDGY", "EDIT",
+ "EDNA", "EGAN", "ELAN", "ELBA", "ELLA", "ELSE", "EMIL", "EMIT",
+ "EMMA", "ENDS", "ERIC", "EROS", "EVEN", "EVER", "EVIL", "EYED",
+ "FACE", "FACT", "FADE", "FAIL", "FAIN", "FAIR", "FAKE", "FALL",
+ "FAME", "FANG", "FARM", "FAST", "FATE", "FAWN", "FEAR", "FEAT",
+ "FEED", "FEEL", "FEET", "FELL", "FELT", "FEND", "FERN", "FEST",
+ "FEUD", "FIEF", "FIGS", "FILE", "FILL", "FILM", "FIND", "FINE",
+ "FINK", "FIRE", "FIRM", "FISH", "FISK", "FIST", "FITS", "FIVE",
+ "FLAG", "FLAK", "FLAM", "FLAT", "FLAW", "FLEA", "FLED", "FLEW",
+ "FLIT", "FLOC", "FLOG", "FLOW", "FLUB", "FLUE", "FOAL", "FOAM",
+ "FOGY", "FOIL", "FOLD", "FOLK", "FOND", "FONT", "FOOD", "FOOL",
+ "FOOT", "FORD", "FORE", "FORK", "FORM", "FORT", "FOSS", "FOUL",
+ "FOUR", "FOWL", "FRAU", "FRAY", "FRED", "FREE", "FRET", "FREY",
+ "FROG", "FROM", "FUEL", "FULL", "FUME", "FUND", "FUNK", "FURY",
+ "FUSE", "FUSS", "GAFF", "GAGE", "GAIL", "GAIN", "GAIT", "GALA",
+ "GALE", "GALL", "GALT", "GAME", "GANG", "GARB", "GARY", "GASH",
+ "GATE", "GAUL", "GAUR", "GAVE", "GAWK", "GEAR", "GELD", "GENE",
+ "GENT", "GERM", "GETS", "GIBE", "GIFT", "GILD", "GILL", "GILT",
+ "GINA", "GIRD", "GIRL", "GIST", "GIVE", "GLAD", "GLEE", "GLEN",
+ "GLIB", "GLOB", "GLOM", "GLOW", "GLUE", "GLUM", "GLUT", "GOAD",
+ "GOAL", "GOAT", "GOER", "GOES", "GOLD", "GOLF", "GONE", "GONG",
+ "GOOD", "GOOF", "GORE", "GORY", "GOSH", "GOUT", "GOWN", "GRAB",
+ "GRAD", "GRAY", "GREG", "GREW", "GREY", "GRID", "GRIM", "GRIN",
+ "GRIT", "GROW", "GRUB", "GULF", "GULL", "GUNK", "GURU", "GUSH",
+ "GUST", "GWEN", "GWYN", "HAAG", "HAAS", "HACK", "HAIL", "HAIR",
+ "HALE", "HALF", "HALL", "HALO", "HALT", "HAND", "HANG", "HANK",
+ "HANS", "HARD", "HARK", "HARM", "HART", "HASH", "HAST", "HATE",
+ "HATH", "HAUL", "HAVE", "HAWK", "HAYS", "HEAD", "HEAL", "HEAR",
+ "HEAT", "HEBE", "HECK", "HEED", "HEEL", "HEFT", "HELD", "HELL",
+ "HELM", "HERB", "HERD", "HERE", "HERO", "HERS", "HESS", "HEWN",
+ "HICK", "HIDE", "HIGH", "HIKE", "HILL", "HILT", "HIND", "HINT",
+ "HIRE", "HISS", "HIVE", "HOBO", "HOCK", "HOFF", "HOLD", "HOLE",
+ "HOLM", "HOLT", "HOME", "HONE", "HONK", "HOOD", "HOOF", "HOOK",
+ "HOOT", "HORN", "HOSE", "HOST", "HOUR", "HOVE", "HOWE", "HOWL",
+ "HOYT", "HUCK", "HUED", "HUFF", "HUGE", "HUGH", "HUGO", "HULK",
+ "HULL", "HUNK", "HUNT", "HURD", "HURL", "HURT", "HUSH", "HYDE",
+ "HYMN", "IBIS", "ICON", "IDEA", "IDLE", "IFFY", "INCA", "INCH",
+ "INTO", "IONS", "IOTA", "IOWA", "IRIS", "IRMA", "IRON", "ISLE",
+ "ITCH", "ITEM", "IVAN", "JACK", "JADE", "JAIL", "JAKE", "JANE",
+ "JAVA", "JEAN", "JEFF", "JERK", "JESS", "JEST", "JIBE", "JILL",
+ "JILT", "JIVE", "JOAN", "JOBS", "JOCK", "JOEL", "JOEY", "JOHN",
+ "JOIN", "JOKE", "JOLT", "JOVE", "JUDD", "JUDE", "JUDO", "JUDY",
+ "JUJU", "JUKE", "JULY", "JUNE", "JUNK", "JUNO", "JURY", "JUST",
+ "JUTE", "KAHN", "KALE", "KANE", "KANT", "KARL", "KATE", "KEEL",
+ "KEEN", "KENO", "KENT", "KERN", "KERR", "KEYS", "KICK", "KILL",
+ "KIND", "KING", "KIRK", "KISS", "KITE", "KLAN", "KNEE", "KNEW",
+ "KNIT", "KNOB", "KNOT", "KNOW", "KOCH", "KONG", "KUDO", "KURD",
+ "KURT", "KYLE", "LACE", "LACK", "LACY", "LADY", "LAID", "LAIN",
+ "LAIR", "LAKE", "LAMB", "LAME", "LAND", "LANE", "LANG", "LARD",
+ "LARK", "LASS", "LAST", "LATE", "LAUD", "LAVA", "LAWN", "LAWS",
+ "LAYS", "LEAD", "LEAF", "LEAK", "LEAN", "LEAR", "LEEK", "LEER",
+ "LEFT", "LEND", "LENS", "LENT", "LEON", "LESK", "LESS", "LEST",
+ "LETS", "LIAR", "LICE", "LICK", "LIED", "LIEN", "LIES", "LIEU",
+ "LIFE", "LIFT", "LIKE", "LILA", "LILT", "LILY", "LIMA", "LIMB",
+ "LIME", "LIND", "LINE", "LINK", "LINT", "LION", "LISA", "LIST",
+ "LIVE", "LOAD", "LOAF", "LOAM", "LOAN", "LOCK", "LOFT", "LOGE",
+ "LOIS", "LOLA", "LONE", "LONG", "LOOK", "LOON", "LOOT", "LORD",
+ "LORE", "LOSE", "LOSS", "LOST", "LOUD", "LOVE", "LOWE", "LUCK",
+ "LUCY", "LUGE", "LUKE", "LULU", "LUND", "LUNG", "LURA", "LURE",
+ "LURK", "LUSH", "LUST", "LYLE", "LYNN", "LYON", "LYRA", "MACE",
+ "MADE", "MAGI", "MAID", "MAIL", "MAIN", "MAKE", "MALE", "MALI",
+ "MALL", "MALT", "MANA", "MANN", "MANY", "MARC", "MARE", "MARK",
+ "MARS", "MART", "MARY", "MASH", "MASK", "MASS", "MAST", "MATE",
+ "MATH", "MAUL", "MAYO", "MEAD", "MEAL", "MEAN", "MEAT", "MEEK",
+ "MEET", "MELD", "MELT", "MEMO", "MEND", "MENU", "MERT", "MESH",
+ "MESS", "MICE", "MIKE", "MILD", "MILE", "MILK", "MILL", "MILT",
+ "MIMI", "MIND", "MINE", "MINI", "MINK", "MINT", "MIRE", "MISS",
+ "MIST", "MITE", "MITT", "MOAN", "MOAT", "MOCK", "MODE", "MOLD",
+ "MOLE", "MOLL", "MOLT", "MONA", "MONK", "MONT", "MOOD", "MOON",
+ "MOOR", "MOOT", "MORE", "MORN", "MORT", "MOSS", "MOST", "MOTH",
+ "MOVE", "MUCH", "MUCK", "MUDD", "MUFF", "MULE", "MULL", "MURK",
+ "MUSH", "MUST", "MUTE", "MUTT", "MYRA", "MYTH", "NAGY", "NAIL",
+ "NAIR", "NAME", "NARY", "NASH", "NAVE", "NAVY", "NEAL", "NEAR",
+ "NEAT", "NECK", "NEED", "NEIL", "NELL", "NEON", "NERO", "NESS",
+ "NEST", "NEWS", "NEWT", "NIBS", "NICE", "NICK", "NILE", "NINA",
+ "NINE", "NOAH", "NODE", "NOEL", "NOLL", "NONE", "NOOK", "NOON",
+ "NORM", "NOSE", "NOTE", "NOUN", "NOVA", "NUDE", "NULL", "NUMB",
+ "OATH", "OBEY", "OBOE", "ODIN", "OHIO", "OILY", "OINT", "OKAY",
+ "OLAF", "OLDY", "OLGA", "OLIN", "OMAN", "OMEN", "OMIT", "ONCE",
+ "ONES", "ONLY", "ONTO", "ONUS", "ORAL", "ORGY", "OSLO", "OTIS",
+ "OTTO", "OUCH", "OUST", "OUTS", "OVAL", "OVEN", "OVER", "OWLY",
+ "OWNS", "QUAD", "QUIT", "QUOD", "RACE", "RACK", "RACY", "RAFT",
+ "RAGE", "RAID", "RAIL", "RAIN", "RAKE", "RANK", "RANT", "RARE",
+ "RASH", "RATE", "RAVE", "RAYS", "READ", "REAL", "REAM", "REAR",
+ "RECK", "REED", "REEF", "REEK", "REEL", "REID", "REIN", "RENA",
+ "REND", "RENT", "REST", "RICE", "RICH", "RICK", "RIDE", "RIFT",
+ "RILL", "RIME", "RING", "RINK", "RISE", "RISK", "RITE", "ROAD",
+ "ROAM", "ROAR", "ROBE", "ROCK", "RODE", "ROIL", "ROLL", "ROME",
+ "ROOD", "ROOF", "ROOK", "ROOM", "ROOT", "ROSA", "ROSE", "ROSS",
+ "ROSY", "ROTH", "ROUT", "ROVE", "ROWE", "ROWS", "RUBE", "RUBY",
+ "RUDE", "RUDY", "RUIN", "RULE", "RUNG", "RUNS", "RUNT", "RUSE",
+ "RUSH", "RUSK", "RUSS", "RUST", "RUTH", "SACK", "SAFE", "SAGE",
+ "SAID", "SAIL", "SALE", "SALK", "SALT", "SAME", "SAND", "SANE",
+ "SANG", "SANK", "SARA", "SAUL", "SAVE", "SAYS", "SCAN", "SCAR",
+ "SCAT", "SCOT", "SEAL", "SEAM", "SEAR", "SEAT", "SEED", "SEEK",
+ "SEEM", "SEEN", "SEES", "SELF", "SELL", "SEND", "SENT", "SETS",
+ "SEWN", "SHAG", "SHAM", "SHAW", "SHAY", "SHED", "SHIM", "SHIN",
+ "SHOD", "SHOE", "SHOT", "SHOW", "SHUN", "SHUT", "SICK", "SIDE",
+ "SIFT", "SIGH", "SIGN", "SILK", "SILL", "SILO", "SILT", "SINE",
+ "SING", "SINK", "SIRE", "SITE", "SITS", "SITU", "SKAT", "SKEW",
+ "SKID", "SKIM", "SKIN", "SKIT", "SLAB", "SLAM", "SLAT", "SLAY",
+ "SLED", "SLEW", "SLID", "SLIM", "SLIT", "SLOB", "SLOG", "SLOT",
+ "SLOW", "SLUG", "SLUM", "SLUR", "SMOG", "SMUG", "SNAG", "SNOB",
+ "SNOW", "SNUB", "SNUG", "SOAK", "SOAR", "SOCK", "SODA", "SOFA",
+ "SOFT", "SOIL", "SOLD", "SOME", "SONG", "SOON", "SOOT", "SORE",
+ "SORT", "SOUL", "SOUR", "SOWN", "STAB", "STAG", "STAN", "STAR",
+ "STAY", "STEM", "STEW", "STIR", "STOW", "STUB", "STUN", "SUCH",
+ "SUDS", "SUIT", "SULK", "SUMS", "SUNG", "SUNK", "SURE", "SURF",
+ "SWAB", "SWAG", "SWAM", "SWAN", "SWAT", "SWAY", "SWIM", "SWUM",
+ "TACK", "TACT", "TAIL", "TAKE", "TALE", "TALK", "TALL", "TANK",
+ "TASK", "TATE", "TAUT", "TEAL", "TEAM", "TEAR", "TECH", "TEEM",
+ "TEEN", "TEET", "TELL", "TEND", "TENT", "TERM", "TERN", "TESS",
+ "TEST", "THAN", "THAT", "THEE", "THEM", "THEN", "THEY", "THIN",
+ "THIS", "THUD", "THUG", "TICK", "TIDE", "TIDY", "TIED", "TIER",
+ "TILE", "TILL", "TILT", "TIME", "TINA", "TINE", "TINT", "TINY",
+ "TIRE", "TOAD", "TOGO", "TOIL", "TOLD", "TOLL", "TONE", "TONG",
+ "TONY", "TOOK", "TOOL", "TOOT", "TORE", "TORN", "TOTE", "TOUR",
+ "TOUT", "TOWN", "TRAG", "TRAM", "TRAY", "TREE", "TREK", "TRIG",
+ "TRIM", "TRIO", "TROD", "TROT", "TROY", "TRUE", "TUBA", "TUBE",
+ "TUCK", "TUFT", "TUNA", "TUNE", "TUNG", "TURF", "TURN", "TUSK",
+ "TWIG", "TWIN", "TWIT", "ULAN", "UNIT", "URGE", "USED", "USER",
+ "USES", "UTAH", "VAIL", "VAIN", "VALE", "VARY", "VASE", "VAST",
+ "VEAL", "VEDA", "VEIL", "VEIN", "VEND", "VENT", "VERB", "VERY",
+ "VETO", "VICE", "VIEW", "VINE", "VISE", "VOID", "VOLT", "VOTE",
+ "WACK", "WADE", "WAGE", "WAIL", "WAIT", "WAKE", "WALE", "WALK",
+ "WALL", "WALT", "WAND", "WANE", "WANG", "WANT", "WARD", "WARM",
+ "WARN", "WART", "WASH", "WAST", "WATS", "WATT", "WAVE", "WAVY",
+ "WAYS", "WEAK", "WEAL", "WEAN", "WEAR", "WEED", "WEEK", "WEIR",
+ "WELD", "WELL", "WELT", "WENT", "WERE", "WERT", "WEST", "WHAM",
+ "WHAT", "WHEE", "WHEN", "WHET", "WHOA", "WHOM", "WICK", "WIFE",
+ "WILD", "WILL", "WIND", "WINE", "WING", "WINK", "WINO", "WIRE",
+ "WISE", "WISH", "WITH", "WOLF", "WONT", "WOOD", "WOOL", "WORD",
+ "WORE", "WORK", "WORM", "WORN", "WOVE", "WRIT", "WYNN", "YALE",
+ "YANG", "YANK", "YARD", "YARN", "YAWL", "YAWN", "YEAH", "YEAR",
+ "YELL", "YOGA", "YOKE" ]
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/RFC1751.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Util/RFC1751.pyi
new file mode 100644
index 00000000..6ad07ff6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/RFC1751.pyi
@@ -0,0 +1,7 @@
+from typing import Dict, List
+
+binary: Dict[int, str]
+wordlist: List[str]
+
+def key_to_english(key: bytes) -> str: ...
+def english_to_key(s: str) -> bytes: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/__init__.py
new file mode 100644
index 00000000..f12214d3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/__init__.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Miscellaneous modules
+
+Contains useful modules that don't belong into any of the
+other Crypto.* subpackages.
+
+======================== =============================================
+Module Description
+======================== =============================================
+`Crypto.Util.number` Number-theoretic functions (primality testing, etc.)
+`Crypto.Util.Counter` Fast counter functions for CTR cipher modes.
+`Crypto.Util.RFC1751` Converts between 128-bit keys and human-readable
+ strings of words.
+`Crypto.Util.asn1` Minimal support for ASN.1 DER encoding
+`Crypto.Util.Padding` Set of functions for adding and removing padding.
+======================== =============================================
+
+:undocumented: _galois, _number_new, cpuid, py3compat, _raw_api
+"""
+
+__all__ = ['RFC1751', 'number', 'strxor', 'asn1', 'Counter', 'Padding']
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/_cpu_features.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_cpu_features.py
new file mode 100644
index 00000000..b3039b58
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_cpu_features.py
@@ -0,0 +1,46 @@
+# ===================================================================
+#
+# Copyright (c) 2018, Helder Eijs
+# 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.Util._raw_api import load_pycryptodome_raw_lib
+
+
+_raw_cpuid_lib = load_pycryptodome_raw_lib("Crypto.Util._cpuid_c",
+ """
+ int have_aes_ni(void);
+ int have_clmul(void);
+ """)
+
+
+def have_aes_ni():
+ return _raw_cpuid_lib.have_aes_ni()
+
+
+def have_clmul():
+ return _raw_cpuid_lib.have_clmul()
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/_cpu_features.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_cpu_features.pyi
new file mode 100644
index 00000000..10e669e3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_cpu_features.pyi
@@ -0,0 +1,2 @@
+def have_aes_ni() -> int: ...
+def have_clmul() -> int: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/_cpuid_c.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_cpuid_c.abi3.so
new file mode 100755
index 00000000..9e41d3cf
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_cpuid_c.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/_file_system.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_file_system.py
new file mode 100644
index 00000000..1cb0c4bf
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_file_system.py
@@ -0,0 +1,54 @@
+# ===================================================================
+#
+# Copyright (c) 2016, Legrandin
+# 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 os
+
+
+def pycryptodome_filename(dir_comps, filename):
+ """Return the complete file name for the module
+
+ dir_comps : list of string
+ The list of directory names in the PyCryptodome package.
+ The first element must be "Crypto".
+
+ filename : string
+ The filename (inclusing extension) in the target directory.
+ """
+
+ if dir_comps[0] != "Crypto":
+ raise ValueError("Only available for modules under 'Crypto'")
+
+ dir_comps = list(dir_comps[1:]) + [filename]
+
+ util_lib, _ = os.path.split(os.path.abspath(__file__))
+ root_lib = os.path.join(util_lib, "..")
+
+ return os.path.join(root_lib, *dir_comps)
+
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/_file_system.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_file_system.pyi
new file mode 100644
index 00000000..d54a1268
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_file_system.pyi
@@ -0,0 +1,4 @@
+from typing import List
+
+
+def pycryptodome_filename(dir_comps: List[str], filename: str) -> str: ...
\ No newline at end of file
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/_raw_api.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_raw_api.py
new file mode 100644
index 00000000..e0065c3d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_raw_api.py
@@ -0,0 +1,325 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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 os
+import abc
+import sys
+from Crypto.Util.py3compat import byte_string
+from Crypto.Util._file_system import pycryptodome_filename
+
+#
+# List of file suffixes for Python extensions
+#
+if sys.version_info[0] < 3:
+
+ import imp
+ extension_suffixes = []
+ for ext, mod, typ in imp.get_suffixes():
+ if typ == imp.C_EXTENSION:
+ extension_suffixes.append(ext)
+
+else:
+
+ from importlib import machinery
+ extension_suffixes = machinery.EXTENSION_SUFFIXES
+
+# Which types with buffer interface we support (apart from byte strings)
+_buffer_type = (bytearray, memoryview)
+
+
+class _VoidPointer(object):
+ @abc.abstractmethod
+ def get(self):
+ """Return the memory location we point to"""
+ return
+
+ @abc.abstractmethod
+ def address_of(self):
+ """Return a raw pointer to this pointer"""
+ return
+
+
+try:
+ # Starting from v2.18, pycparser (used by cffi for in-line ABI mode)
+ # stops working correctly when PYOPTIMIZE==2 or the parameter -OO is
+ # passed. In that case, we fall back to ctypes.
+ # Note that PyPy ships with an old version of pycparser so we can keep
+ # using cffi there.
+ # See https://github.com/Legrandin/pycryptodome/issues/228
+ if '__pypy__' not in sys.builtin_module_names and sys.flags.optimize == 2:
+ raise ImportError("CFFI with optimize=2 fails due to pycparser bug.")
+
+ # cffi still uses PyUnicode_GetSize, which was removed in Python 3.12
+ # thus leading to a crash on cffi.dlopen()
+ # See https://groups.google.com/u/1/g/python-cffi/c/oZkOIZ_zi5k
+ if sys.version_info >= (3, 12) and os.name == "nt":
+ raise ImportError("CFFI is not compatible with Python 3.12 on Windows")
+
+ from cffi import FFI
+
+ ffi = FFI()
+ null_pointer = ffi.NULL
+ uint8_t_type = ffi.typeof(ffi.new("const uint8_t*"))
+
+ _Array = ffi.new("uint8_t[1]").__class__.__bases__
+
+ def load_lib(name, cdecl):
+ """Load a shared library and return a handle to it.
+
+ @name, either an absolute path or the name of a library
+ in the system search path.
+
+ @cdecl, the C function declarations.
+ """
+
+ if hasattr(ffi, "RTLD_DEEPBIND") and not os.getenv('PYCRYPTODOME_DISABLE_DEEPBIND'):
+ lib = ffi.dlopen(name, ffi.RTLD_DEEPBIND)
+ else:
+ lib = ffi.dlopen(name)
+ ffi.cdef(cdecl)
+ return lib
+
+ def c_ulong(x):
+ """Convert a Python integer to unsigned long"""
+ return x
+
+ c_ulonglong = c_ulong
+ c_uint = c_ulong
+ c_ubyte = c_ulong
+
+ def c_size_t(x):
+ """Convert a Python integer to size_t"""
+ return x
+
+ def create_string_buffer(init_or_size, size=None):
+ """Allocate the given amount of bytes (initially set to 0)"""
+
+ if isinstance(init_or_size, bytes):
+ size = max(len(init_or_size) + 1, size)
+ result = ffi.new("uint8_t[]", size)
+ result[:] = init_or_size
+ else:
+ if size:
+ raise ValueError("Size must be specified once only")
+ result = ffi.new("uint8_t[]", init_or_size)
+ return result
+
+ def get_c_string(c_string):
+ """Convert a C string into a Python byte sequence"""
+ return ffi.string(c_string)
+
+ def get_raw_buffer(buf):
+ """Convert a C buffer into a Python byte sequence"""
+ return ffi.buffer(buf)[:]
+
+ def c_uint8_ptr(data):
+ if isinstance(data, _buffer_type):
+ # This only works for cffi >= 1.7
+ return ffi.cast(uint8_t_type, ffi.from_buffer(data))
+ elif byte_string(data) or isinstance(data, _Array):
+ return data
+ else:
+ raise TypeError("Object type %s cannot be passed to C code" % type(data))
+
+ class VoidPointer_cffi(_VoidPointer):
+ """Model a newly allocated pointer to void"""
+
+ def __init__(self):
+ self._pp = ffi.new("void *[1]")
+
+ def get(self):
+ return self._pp[0]
+
+ def address_of(self):
+ return self._pp
+
+ def VoidPointer():
+ return VoidPointer_cffi()
+
+ backend = "cffi"
+
+except ImportError:
+
+ import ctypes
+ from ctypes import (CDLL, c_void_p, byref, c_ulong, c_ulonglong, c_size_t,
+ create_string_buffer, c_ubyte, c_uint)
+ from ctypes.util import find_library
+ from ctypes import Array as _Array
+
+ null_pointer = None
+ cached_architecture = []
+
+ def c_ubyte(c):
+ if not (0 <= c < 256):
+ raise OverflowError()
+ return ctypes.c_ubyte(c)
+
+ def load_lib(name, cdecl):
+ if not cached_architecture:
+ # platform.architecture() creates a subprocess, so caching the
+ # result makes successive imports faster.
+ import platform
+ cached_architecture[:] = platform.architecture()
+ bits, linkage = cached_architecture
+ if "." not in name and not linkage.startswith("Win"):
+ full_name = find_library(name)
+ if full_name is None:
+ raise OSError("Cannot load library '%s'" % name)
+ name = full_name
+ return CDLL(name)
+
+ def get_c_string(c_string):
+ return c_string.value
+
+ def get_raw_buffer(buf):
+ return buf.raw
+
+ # ---- Get raw pointer ---
+
+ _c_ssize_t = ctypes.c_ssize_t
+
+ _PyBUF_SIMPLE = 0
+ _PyObject_GetBuffer = ctypes.pythonapi.PyObject_GetBuffer
+ _PyBuffer_Release = ctypes.pythonapi.PyBuffer_Release
+ _py_object = ctypes.py_object
+ _c_ssize_p = ctypes.POINTER(_c_ssize_t)
+
+ # See Include/object.h for CPython
+ # and https://github.com/pallets/click/blob/master/src/click/_winconsole.py
+ class _Py_buffer(ctypes.Structure):
+ _fields_ = [
+ ('buf', c_void_p),
+ ('obj', ctypes.py_object),
+ ('len', _c_ssize_t),
+ ('itemsize', _c_ssize_t),
+ ('readonly', ctypes.c_int),
+ ('ndim', ctypes.c_int),
+ ('format', ctypes.c_char_p),
+ ('shape', _c_ssize_p),
+ ('strides', _c_ssize_p),
+ ('suboffsets', _c_ssize_p),
+ ('internal', c_void_p)
+ ]
+
+ # Extra field for CPython 2.6/2.7
+ if sys.version_info[0] == 2:
+ _fields_.insert(-1, ('smalltable', _c_ssize_t * 2))
+
+ def c_uint8_ptr(data):
+ if byte_string(data) or isinstance(data, _Array):
+ return data
+ elif isinstance(data, _buffer_type):
+ obj = _py_object(data)
+ buf = _Py_buffer()
+ _PyObject_GetBuffer(obj, byref(buf), _PyBUF_SIMPLE)
+ try:
+ buffer_type = ctypes.c_ubyte * buf.len
+ return buffer_type.from_address(buf.buf)
+ finally:
+ _PyBuffer_Release(byref(buf))
+ else:
+ raise TypeError("Object type %s cannot be passed to C code" % type(data))
+
+ # ---
+
+ class VoidPointer_ctypes(_VoidPointer):
+ """Model a newly allocated pointer to void"""
+
+ def __init__(self):
+ self._p = c_void_p()
+
+ def get(self):
+ return self._p
+
+ def address_of(self):
+ return byref(self._p)
+
+ def VoidPointer():
+ return VoidPointer_ctypes()
+
+ backend = "ctypes"
+
+
+class SmartPointer(object):
+ """Class to hold a non-managed piece of memory"""
+
+ def __init__(self, raw_pointer, destructor):
+ self._raw_pointer = raw_pointer
+ self._destructor = destructor
+
+ def get(self):
+ return self._raw_pointer
+
+ def release(self):
+ rp, self._raw_pointer = self._raw_pointer, None
+ return rp
+
+ def __del__(self):
+ try:
+ if self._raw_pointer is not None:
+ self._destructor(self._raw_pointer)
+ self._raw_pointer = None
+ except AttributeError:
+ pass
+
+
+def load_pycryptodome_raw_lib(name, cdecl):
+ """Load a shared library and return a handle to it.
+
+ @name, the name of the library expressed as a PyCryptodome module,
+ for instance Crypto.Cipher._raw_cbc.
+
+ @cdecl, the C function declarations.
+ """
+
+ split = name.split(".")
+ dir_comps, basename = split[:-1], split[-1]
+ attempts = []
+ for ext in extension_suffixes:
+ try:
+ filename = basename + ext
+ full_name = pycryptodome_filename(dir_comps, filename)
+ if not os.path.isfile(full_name):
+ attempts.append("Not found '%s'" % filename)
+ continue
+ return load_lib(full_name, cdecl)
+ except OSError as exp:
+ attempts.append("Cannot load '%s': %s" % (filename, str(exp)))
+ raise OSError("Cannot load native module '%s': %s" % (name, ", ".join(attempts)))
+
+
+def is_buffer(x):
+ """Return True if object x supports the buffer interface"""
+ return isinstance(x, (bytes, bytearray, memoryview))
+
+
+def is_writeable_buffer(x):
+ return (isinstance(x, bytearray) or
+ (isinstance(x, memoryview) and not x.readonly))
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/_raw_api.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_raw_api.pyi
new file mode 100644
index 00000000..2bc53016
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_raw_api.pyi
@@ -0,0 +1,27 @@
+from typing import Any, Optional, Union
+
+def load_lib(name: str, cdecl: str) -> Any : ...
+def c_ulong(x: int ) -> Any : ...
+def c_ulonglong(x: int ) -> Any : ...
+def c_size_t(x: int) -> Any : ...
+def create_string_buffer(init_or_size: Union[bytes,int], size: Optional[int]) -> Any : ...
+def get_c_string(c_string: Any) -> bytes : ...
+def get_raw_buffer(buf: Any) -> bytes : ...
+def c_uint8_ptr(data: Union[bytes, memoryview, bytearray]) -> Any : ...
+
+class VoidPointer(object):
+ def get(self) -> Any : ...
+ def address_of(self) -> Any : ...
+
+class SmartPointer(object):
+ def __init__(self, raw_pointer: Any, destructor: Any) -> None : ...
+ def get(self) -> Any : ...
+ def release(self) -> Any : ...
+
+backend : str
+null_pointer : Any
+ffi: Any
+
+def load_pycryptodome_raw_lib(name: str, cdecl: str) -> Any : ...
+def is_buffer(x: Any) -> bool : ...
+def is_writeable_buffer(x: Any) -> bool : ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/_strxor.abi3.so b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_strxor.abi3.so
new file mode 100755
index 00000000..85b54966
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/Crypto/Util/_strxor.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/asn1.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/asn1.py
new file mode 100644
index 00000000..887cf1ad
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/asn1.py
@@ -0,0 +1,1064 @@
+# -*- coding: utf-8 -*-
+#
+# Util/asn1.py : Minimal support for ASN.1 DER binary encoding.
+#
+# ===================================================================
+# 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 struct
+
+from Crypto.Util.py3compat import byte_string, bchr, bord
+
+from Crypto.Util.number import long_to_bytes, bytes_to_long
+
+__all__ = ['DerObject', 'DerInteger', 'DerBoolean', 'DerOctetString',
+ 'DerNull', 'DerSequence', 'DerObjectId', 'DerBitString', 'DerSetOf']
+
+# Useful references:
+# - https://luca.ntop.org/Teaching/Appunti/asn1.html
+# - https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/
+# - https://www.zytrax.com/tech/survival/asn1.html
+# - https://www.oss.com/asn1/resources/books-whitepapers-pubs/larmouth-asn1-book.pdf
+# - https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf
+# - https://misc.daniel-marschall.de/asn.1/oid-converter/online.php
+
+def _is_number(x, only_non_negative=False):
+ test = 0
+ try:
+ test = x + test
+ except TypeError:
+ return False
+ return not only_non_negative or x >= 0
+
+
+class BytesIO_EOF(object):
+ """This class differs from BytesIO in that a ValueError exception is
+ raised whenever EOF is reached."""
+
+ def __init__(self, initial_bytes):
+ self._buffer = initial_bytes
+ self._index = 0
+ self._bookmark = None
+
+ def set_bookmark(self):
+ self._bookmark = self._index
+
+ def data_since_bookmark(self):
+ assert self._bookmark is not None
+ return self._buffer[self._bookmark:self._index]
+
+ def remaining_data(self):
+ return len(self._buffer) - self._index
+
+ def read(self, length):
+ new_index = self._index + length
+ if new_index > len(self._buffer):
+ raise ValueError("Not enough data for DER decoding: expected %d bytes and found %d" % (new_index, len(self._buffer)))
+
+ result = self._buffer[self._index:new_index]
+ self._index = new_index
+ return result
+
+ def read_byte(self):
+ return bord(self.read(1)[0])
+
+
+class DerObject(object):
+ """Base class for defining a single DER object.
+
+ This class should never be directly instantiated.
+ """
+
+ def __init__(self, asn1Id=None, payload=b'', implicit=None,
+ constructed=False, explicit=None):
+ """Initialize the DER object according to a specific ASN.1 type.
+
+ :Parameters:
+ asn1Id : integer or byte
+ The universal DER tag number for this object
+ (e.g. 0x10 for a SEQUENCE).
+ If None, the tag is not known yet.
+
+ payload : byte string
+ The initial payload of the object (that it,
+ the content octets).
+ If not specified, the payload is empty.
+
+ implicit : integer or byte
+ The IMPLICIT tag number (< 0x1F) to use for the encoded object.
+ It overrides the universal tag *asn1Id*.
+ It cannot be combined with the ``explicit`` parameter.
+ By default, there is no IMPLICIT tag.
+
+ constructed : bool
+ True when the ASN.1 type is *constructed*.
+ False when it is *primitive* (default).
+
+ explicit : integer or byte
+ The EXPLICIT tag number (< 0x1F) to use for the encoded object.
+ It cannot be combined with the ``implicit`` parameter.
+ By default, there is no EXPLICIT tag.
+ """
+
+ if asn1Id is None:
+ # The tag octet will be read in with ``decode``
+ self._tag_octet = None
+ return
+ asn1Id = self._convertTag(asn1Id)
+
+ self.payload = payload
+
+ # In a BER/DER identifier octet:
+ # * bits 4-0 contain the tag value
+ # * bit 5 is set if the type is 'constructed'
+ # and unset if 'primitive'
+ # * bits 7-6 depend on the encoding class
+ #
+ # Class | Bit 7, Bit 6
+ # ----------------------------------
+ # universal | 0 0
+ # application | 0 1
+ # context-spec | 1 0 (default for IMPLICIT/EXPLICIT)
+ # private | 1 1
+ #
+
+ constructed_bit = 0x20 if constructed else 0x00
+
+ if None not in (explicit, implicit):
+ raise ValueError("Explicit and implicit tags are"
+ " mutually exclusive")
+
+ if implicit is not None:
+ # IMPLICIT tag overrides asn1Id
+ self._tag_octet = 0x80 | constructed_bit | self._convertTag(implicit)
+ elif explicit is not None:
+ # 'constructed bit' is always asserted for an EXPLICIT tag
+ self._tag_octet = 0x80 | 0x20 | self._convertTag(explicit)
+ self._inner_tag_octet = constructed_bit | asn1Id
+ else:
+ # Neither IMPLICIT nor EXPLICIT
+ self._tag_octet = constructed_bit | asn1Id
+
+ def _convertTag(self, tag):
+ """Check if *tag* is a real DER tag (5 bits).
+ Convert it from a character to number if necessary.
+ """
+ if not _is_number(tag):
+ if len(tag) == 1:
+ tag = bord(tag[0])
+ # Ensure that tag is a low tag
+ if not (_is_number(tag) and 0 <= tag < 0x1F):
+ raise ValueError("Wrong DER tag")
+ return tag
+
+ @staticmethod
+ def _definite_form(length):
+ """Build length octets according to BER/DER
+ definite form.
+ """
+ if length > 127:
+ encoding = long_to_bytes(length)
+ return bchr(len(encoding) + 128) + encoding
+ return bchr(length)
+
+ def encode(self):
+ """Return this DER element, fully encoded as a binary byte string."""
+
+ # Concatenate identifier octets, length octets,
+ # and contents octets
+
+ output_payload = self.payload
+
+ # In case of an EXTERNAL tag, first encode the inner
+ # element.
+ if hasattr(self, "_inner_tag_octet"):
+ output_payload = (bchr(self._inner_tag_octet) +
+ self._definite_form(len(self.payload)) +
+ self.payload)
+
+ return (bchr(self._tag_octet) +
+ self._definite_form(len(output_payload)) +
+ output_payload)
+
+ def _decodeLen(self, s):
+ """Decode DER length octets from a file."""
+
+ length = s.read_byte()
+
+ if length > 127:
+ encoded_length = s.read(length & 0x7F)
+ if bord(encoded_length[0]) == 0:
+ raise ValueError("Invalid DER: length has leading zero")
+ length = bytes_to_long(encoded_length)
+ if length <= 127:
+ raise ValueError("Invalid DER: length in long form but smaller than 128")
+
+ return length
+
+ def decode(self, der_encoded, strict=False):
+ """Decode a complete DER element, and re-initializes this
+ object with it.
+
+ Args:
+ der_encoded (byte string): A complete DER element.
+
+ Raises:
+ ValueError: in case of parsing errors.
+ """
+
+ if not byte_string(der_encoded):
+ raise ValueError("Input is not a byte string")
+
+ s = BytesIO_EOF(der_encoded)
+ self._decodeFromStream(s, strict)
+
+ # There shouldn't be other bytes left
+ if s.remaining_data() > 0:
+ raise ValueError("Unexpected extra data after the DER structure")
+
+ return self
+
+ def _decodeFromStream(self, s, strict):
+ """Decode a complete DER element from a file."""
+
+ idOctet = s.read_byte()
+ if self._tag_octet is not None:
+ if idOctet != self._tag_octet:
+ raise ValueError("Unexpected DER tag")
+ else:
+ self._tag_octet = idOctet
+ length = self._decodeLen(s)
+ self.payload = s.read(length)
+
+ # In case of an EXTERNAL tag, further decode the inner
+ # element.
+ if hasattr(self, "_inner_tag_octet"):
+ p = BytesIO_EOF(self.payload)
+ inner_octet = p.read_byte()
+ if inner_octet != self._inner_tag_octet:
+ raise ValueError("Unexpected internal DER tag")
+ length = self._decodeLen(p)
+ self.payload = p.read(length)
+
+ # There shouldn't be other bytes left
+ if p.remaining_data() > 0:
+ raise ValueError("Unexpected extra data after the DER structure")
+
+
+class DerInteger(DerObject):
+ """Class to model a DER INTEGER.
+
+ An example of encoding is::
+
+ >>> from Crypto.Util.asn1 import DerInteger
+ >>> from binascii import hexlify, unhexlify
+ >>> int_der = DerInteger(9)
+ >>> print hexlify(int_der.encode())
+
+ which will show ``020109``, the DER encoding of 9.
+
+ And for decoding::
+
+ >>> s = unhexlify(b'020109')
+ >>> try:
+ >>> int_der = DerInteger()
+ >>> int_der.decode(s)
+ >>> print int_der.value
+ >>> except ValueError:
+ >>> print "Not a valid DER INTEGER"
+
+ the output will be ``9``.
+
+ :ivar value: The integer value
+ :vartype value: integer
+ """
+
+ def __init__(self, value=0, implicit=None, explicit=None):
+ """Initialize the DER object as an INTEGER.
+
+ :Parameters:
+ value : integer
+ The value of the integer.
+
+ implicit : integer
+ The IMPLICIT tag to use for the encoded object.
+ It overrides the universal tag for INTEGER (2).
+ """
+
+ DerObject.__init__(self, 0x02, b'', implicit,
+ False, explicit)
+ self.value = value # The integer value
+
+ def encode(self):
+ """Return the DER INTEGER, fully encoded as a
+ binary string."""
+
+ number = self.value
+ self.payload = b''
+ while True:
+ self.payload = bchr(int(number & 255)) + self.payload
+ if 128 <= number <= 255:
+ self.payload = bchr(0x00) + self.payload
+ if -128 <= number <= 255:
+ break
+ number >>= 8
+ return DerObject.encode(self)
+
+ def decode(self, der_encoded, strict=False):
+ """Decode a DER-encoded INTEGER, and re-initializes this
+ object with it.
+
+ Args:
+ der_encoded (byte string): A complete INTEGER DER element.
+
+ Raises:
+ ValueError: in case of parsing errors.
+ """
+
+ return DerObject.decode(self, der_encoded, strict=strict)
+
+ def _decodeFromStream(self, s, strict):
+ """Decode a complete DER INTEGER from a file."""
+
+ # Fill up self.payload
+ DerObject._decodeFromStream(self, s, strict)
+
+ if strict:
+ if len(self.payload) == 0:
+ raise ValueError("Invalid encoding for DER INTEGER: empty payload")
+ if len(self.payload) >= 2 and struct.unpack('>H', self.payload[:2])[0] < 0x80:
+ raise ValueError("Invalid encoding for DER INTEGER: leading zero")
+
+ # Derive self.value from self.payload
+ self.value = 0
+ bits = 1
+ for i in self.payload:
+ self.value *= 256
+ self.value += bord(i)
+ bits <<= 8
+ if self.payload and bord(self.payload[0]) & 0x80:
+ self.value -= bits
+
+
+class DerBoolean(DerObject):
+ """Class to model a DER-encoded BOOLEAN.
+
+ An example of encoding is::
+
+ >>> from Crypto.Util.asn1 import DerBoolean
+ >>> bool_der = DerBoolean(True)
+ >>> print(bool_der.encode().hex())
+
+ which will show ``0101ff``, the DER encoding of True.
+
+ And for decoding::
+
+ >>> s = bytes.fromhex('0101ff')
+ >>> try:
+ >>> bool_der = DerBoolean()
+ >>> bool_der.decode(s)
+ >>> print(bool_der.value)
+ >>> except ValueError:
+ >>> print "Not a valid DER BOOLEAN"
+
+ the output will be ``True``.
+
+ :ivar value: The boolean value
+ :vartype value: boolean
+ """
+ def __init__(self, value=False, implicit=None, explicit=None):
+ """Initialize the DER object as a BOOLEAN.
+
+ Args:
+ value (boolean):
+ The value of the boolean. Default is False.
+
+ implicit (integer or byte):
+ The IMPLICIT tag number (< 0x1F) to use for the encoded object.
+ It overrides the universal tag for BOOLEAN (1).
+ It cannot be combined with the ``explicit`` parameter.
+ By default, there is no IMPLICIT tag.
+
+ explicit (integer or byte):
+ The EXPLICIT tag number (< 0x1F) to use for the encoded object.
+ It cannot be combined with the ``implicit`` parameter.
+ By default, there is no EXPLICIT tag.
+ """
+
+ DerObject.__init__(self, 0x01, b'', implicit, False, explicit)
+ self.value = value # The boolean value
+
+ def encode(self):
+ """Return the DER BOOLEAN, fully encoded as a binary string."""
+
+ self.payload = b'\xFF' if self.value else b'\x00'
+ return DerObject.encode(self)
+
+ def decode(self, der_encoded, strict=False):
+ """Decode a DER-encoded BOOLEAN, and re-initializes this object with it.
+
+ Args:
+ der_encoded (byte string): A DER-encoded BOOLEAN.
+
+ Raises:
+ ValueError: in case of parsing errors.
+ """
+
+ return DerObject.decode(self, der_encoded, strict)
+
+ def _decodeFromStream(self, s, strict):
+ """Decode a DER-encoded BOOLEAN from a file."""
+
+ # Fill up self.payload
+ DerObject._decodeFromStream(self, s, strict)
+
+ if len(self.payload) != 1:
+ raise ValueError("Invalid encoding for DER BOOLEAN: payload is not 1 byte")
+
+ if bord(self.payload[0]) == 0:
+ self.value = False
+ elif bord(self.payload[0]) == 0xFF:
+ self.value = True
+ else:
+ raise ValueError("Invalid payload for DER BOOLEAN")
+
+
+class DerSequence(DerObject):
+ """Class to model a DER SEQUENCE.
+
+ This object behaves like a dynamic Python sequence.
+
+ Sub-elements that are INTEGERs behave like Python integers.
+
+ Any other sub-element is a binary string encoded as a complete DER
+ sub-element (TLV).
+
+ An example of encoding is:
+
+ >>> from Crypto.Util.asn1 import DerSequence, DerInteger
+ >>> from binascii import hexlify, unhexlify
+ >>> obj_der = unhexlify('070102')
+ >>> seq_der = DerSequence([4])
+ >>> seq_der.append(9)
+ >>> seq_der.append(obj_der.encode())
+ >>> print hexlify(seq_der.encode())
+
+ which will show ``3009020104020109070102``, the DER encoding of the
+ sequence containing ``4``, ``9``, and the object with payload ``02``.
+
+ For decoding:
+
+ >>> s = unhexlify(b'3009020104020109070102')
+ >>> try:
+ >>> seq_der = DerSequence()
+ >>> seq_der.decode(s)
+ >>> print len(seq_der)
+ >>> print seq_der[0]
+ >>> print seq_der[:]
+ >>> except ValueError:
+ >>> print "Not a valid DER SEQUENCE"
+
+ the output will be::
+
+ 3
+ 4
+ [4, 9, b'\x07\x01\x02']
+
+ """
+
+ def __init__(self, startSeq=None, implicit=None, explicit=None):
+ """Initialize the DER object as a SEQUENCE.
+
+ :Parameters:
+ startSeq : Python sequence
+ A sequence whose element are either integers or
+ other DER objects.
+
+ implicit : integer or byte
+ The IMPLICIT tag number (< 0x1F) to use for the encoded object.
+ It overrides the universal tag for SEQUENCE (16).
+ It cannot be combined with the ``explicit`` parameter.
+ By default, there is no IMPLICIT tag.
+
+ explicit : integer or byte
+ The EXPLICIT tag number (< 0x1F) to use for the encoded object.
+ It cannot be combined with the ``implicit`` parameter.
+ By default, there is no EXPLICIT tag.
+ """
+
+ DerObject.__init__(self, 0x10, b'', implicit, True, explicit)
+ if startSeq is None:
+ self._seq = []
+ else:
+ self._seq = startSeq
+
+ # A few methods to make it behave like a python sequence
+
+ def __delitem__(self, n):
+ del self._seq[n]
+
+ def __getitem__(self, n):
+ return self._seq[n]
+
+ def __setitem__(self, key, value):
+ self._seq[key] = value
+
+ def __setslice__(self, i, j, sequence):
+ self._seq[i:j] = sequence
+
+ def __delslice__(self, i, j):
+ del self._seq[i:j]
+
+ def __getslice__(self, i, j):
+ return self._seq[max(0, i):max(0, j)]
+
+ def __len__(self):
+ return len(self._seq)
+
+ def __iadd__(self, item):
+ self._seq.append(item)
+ return self
+
+ def append(self, item):
+ self._seq.append(item)
+ return self
+
+ def insert(self, index, item):
+ self._seq.insert(index, item)
+ return self
+
+ def hasInts(self, only_non_negative=True):
+ """Return the number of items in this sequence that are
+ integers.
+
+ Args:
+ only_non_negative (boolean):
+ If ``True``, negative integers are not counted in.
+ """
+
+ items = [x for x in self._seq if _is_number(x, only_non_negative)]
+ return len(items)
+
+ def hasOnlyInts(self, only_non_negative=True):
+ """Return ``True`` if all items in this sequence are integers
+ or non-negative integers.
+
+ This function returns False is the sequence is empty,
+ or at least one member is not an integer.
+
+ Args:
+ only_non_negative (boolean):
+ If ``True``, the presence of negative integers
+ causes the method to return ``False``."""
+ return self._seq and self.hasInts(only_non_negative) == len(self._seq)
+
+ def encode(self):
+ """Return this DER SEQUENCE, fully encoded as a
+ binary string.
+
+ Raises:
+ ValueError: if some elements in the sequence are neither integers
+ nor byte strings.
+ """
+ self.payload = b''
+ for item in self._seq:
+ if byte_string(item):
+ self.payload += item
+ elif _is_number(item):
+ self.payload += DerInteger(item).encode()
+ else:
+ self.payload += item.encode()
+ return DerObject.encode(self)
+
+ def decode(self, der_encoded, strict=False, nr_elements=None, only_ints_expected=False):
+ """Decode a complete DER SEQUENCE, and re-initializes this
+ object with it.
+
+ Args:
+ der_encoded (byte string):
+ A complete SEQUENCE DER element.
+ nr_elements (None or integer or list of integers):
+ The number of members the SEQUENCE can have
+ only_ints_expected (boolean):
+ Whether the SEQUENCE is expected to contain only integers.
+ strict (boolean):
+ Whether decoding must check for strict DER compliancy.
+
+ Raises:
+ ValueError: in case of parsing errors.
+
+ DER INTEGERs are decoded into Python integers. Any other DER
+ element is not decoded. Its validity is not checked.
+ """
+
+ self._nr_elements = nr_elements
+ result = DerObject.decode(self, der_encoded, strict=strict)
+
+ if only_ints_expected and not self.hasOnlyInts():
+ raise ValueError("Some members are not INTEGERs")
+
+ return result
+
+ def _decodeFromStream(self, s, strict):
+ """Decode a complete DER SEQUENCE from a file."""
+
+ self._seq = []
+
+ # Fill up self.payload
+ DerObject._decodeFromStream(self, s, strict)
+
+ # Add one item at a time to self.seq, by scanning self.payload
+ p = BytesIO_EOF(self.payload)
+ while p.remaining_data() > 0:
+ p.set_bookmark()
+
+ der = DerObject()
+ der._decodeFromStream(p, strict)
+
+ # Parse INTEGERs differently
+ if der._tag_octet != 0x02:
+ self._seq.append(p.data_since_bookmark())
+ else:
+ derInt = DerInteger()
+ data = p.data_since_bookmark()
+ derInt.decode(data, strict=strict)
+ self._seq.append(derInt.value)
+
+ ok = True
+ if self._nr_elements is not None:
+ try:
+ ok = len(self._seq) in self._nr_elements
+ except TypeError:
+ ok = len(self._seq) == self._nr_elements
+
+ if not ok:
+ raise ValueError("Unexpected number of members (%d)"
+ " in the sequence" % len(self._seq))
+
+
+class DerOctetString(DerObject):
+ """Class to model a DER OCTET STRING.
+
+ An example of encoding is:
+
+ >>> from Crypto.Util.asn1 import DerOctetString
+ >>> from binascii import hexlify, unhexlify
+ >>> os_der = DerOctetString(b'\\xaa')
+ >>> os_der.payload += b'\\xbb'
+ >>> print hexlify(os_der.encode())
+
+ which will show ``0402aabb``, the DER encoding for the byte string
+ ``b'\\xAA\\xBB'``.
+
+ For decoding:
+
+ >>> s = unhexlify(b'0402aabb')
+ >>> try:
+ >>> os_der = DerOctetString()
+ >>> os_der.decode(s)
+ >>> print hexlify(os_der.payload)
+ >>> except ValueError:
+ >>> print "Not a valid DER OCTET STRING"
+
+ the output will be ``aabb``.
+
+ :ivar payload: The content of the string
+ :vartype payload: byte string
+ """
+
+ def __init__(self, value=b'', implicit=None):
+ """Initialize the DER object as an OCTET STRING.
+
+ :Parameters:
+ value : byte string
+ The initial payload of the object.
+ If not specified, the payload is empty.
+
+ implicit : integer
+ The IMPLICIT tag to use for the encoded object.
+ It overrides the universal tag for OCTET STRING (4).
+ """
+ DerObject.__init__(self, 0x04, value, implicit, False)
+
+
+class DerNull(DerObject):
+ """Class to model a DER NULL element."""
+
+ def __init__(self):
+ """Initialize the DER object as a NULL."""
+
+ DerObject.__init__(self, 0x05, b'', None, False)
+
+
+class DerObjectId(DerObject):
+ """Class to model a DER OBJECT ID.
+
+ An example of encoding is:
+
+ >>> from Crypto.Util.asn1 import DerObjectId
+ >>> from binascii import hexlify, unhexlify
+ >>> oid_der = DerObjectId("1.2")
+ >>> oid_der.value += ".840.113549.1.1.1"
+ >>> print hexlify(oid_der.encode())
+
+ which will show ``06092a864886f70d010101``, the DER encoding for the
+ RSA Object Identifier ``1.2.840.113549.1.1.1``.
+
+ For decoding:
+
+ >>> s = unhexlify(b'06092a864886f70d010101')
+ >>> try:
+ >>> oid_der = DerObjectId()
+ >>> oid_der.decode(s)
+ >>> print oid_der.value
+ >>> except ValueError:
+ >>> print "Not a valid DER OBJECT ID"
+
+ the output will be ``1.2.840.113549.1.1.1``.
+
+ :ivar value: The Object ID (OID), a dot separated list of integers
+ :vartype value: string
+ """
+
+ def __init__(self, value='', implicit=None, explicit=None):
+ """Initialize the DER object as an OBJECT ID.
+
+ :Parameters:
+ value : string
+ The initial Object Identifier (e.g. "1.2.0.0.6.2").
+ implicit : integer
+ The IMPLICIT tag to use for the encoded object.
+ It overrides the universal tag for OBJECT ID (6).
+ explicit : integer
+ The EXPLICIT tag to use for the encoded object.
+ """
+ DerObject.__init__(self, 0x06, b'', implicit, False, explicit)
+ self.value = value
+
+ def encode(self):
+ """Return the DER OBJECT ID, fully encoded as a
+ binary string."""
+
+ comps = [int(x) for x in self.value.split(".")]
+
+ if len(comps) < 2:
+ raise ValueError("Not a valid Object Identifier string")
+ if comps[0] > 2:
+ raise ValueError("First component must be 0, 1 or 2")
+ if comps[0] < 2 and comps[1] > 39:
+ raise ValueError("Second component must be 39 at most")
+
+ subcomps = [40 * comps[0] + comps[1]] + comps[2:]
+
+ encoding = []
+ for v in reversed(subcomps):
+ encoding.append(v & 0x7F)
+ v >>= 7
+ while v:
+ encoding.append((v & 0x7F) | 0x80)
+ v >>= 7
+
+ self.payload = b''.join([bchr(x) for x in reversed(encoding)])
+ return DerObject.encode(self)
+
+ def decode(self, der_encoded, strict=False):
+ """Decode a complete DER OBJECT ID, and re-initializes this
+ object with it.
+
+ Args:
+ der_encoded (byte string):
+ A complete DER OBJECT ID.
+ strict (boolean):
+ Whether decoding must check for strict DER compliancy.
+
+ Raises:
+ ValueError: in case of parsing errors.
+ """
+
+ return DerObject.decode(self, der_encoded, strict)
+
+ def _decodeFromStream(self, s, strict):
+ """Decode a complete DER OBJECT ID from a file."""
+
+ # Fill up self.payload
+ DerObject._decodeFromStream(self, s, strict)
+
+ # Derive self.value from self.payload
+ p = BytesIO_EOF(self.payload)
+
+ subcomps = []
+ v = 0
+ while p.remaining_data():
+ c = p.read_byte()
+ v = (v << 7) + (c & 0x7F)
+ if not (c & 0x80):
+ subcomps.append(v)
+ v = 0
+
+ if len(subcomps) == 0:
+ raise ValueError("Empty payload")
+
+ if subcomps[0] < 40:
+ subcomps[:1] = [0, subcomps[0]]
+ elif subcomps[0] < 80:
+ subcomps[:1] = [1, subcomps[0] - 40]
+ else:
+ subcomps[:1] = [2, subcomps[0] - 80]
+
+ self.value = ".".join([str(x) for x in subcomps])
+
+
+class DerBitString(DerObject):
+ """Class to model a DER BIT STRING.
+
+ An example of encoding is:
+
+ >>> from Crypto.Util.asn1 import DerBitString
+ >>> bs_der = DerBitString(b'\\xAA')
+ >>> bs_der.value += b'\\xBB'
+ >>> print(bs_der.encode().hex())
+
+ which will show ``030300aabb``, the DER encoding for the bit string
+ ``b'\\xAA\\xBB'``.
+
+ For decoding:
+
+ >>> s = bytes.fromhex('030300aabb')
+ >>> try:
+ >>> bs_der = DerBitString()
+ >>> bs_der.decode(s)
+ >>> print(bs_der.value.hex())
+ >>> except ValueError:
+ >>> print "Not a valid DER BIT STRING"
+
+ the output will be ``aabb``.
+
+ :ivar value: The content of the string
+ :vartype value: byte string
+ """
+
+ def __init__(self, value=b'', implicit=None, explicit=None):
+ """Initialize the DER object as a BIT STRING.
+
+ :Parameters:
+ value : byte string or DER object
+ The initial, packed bit string.
+ If not specified, the bit string is empty.
+ implicit : integer
+ The IMPLICIT tag to use for the encoded object.
+ It overrides the universal tag for BIT STRING (3).
+ explicit : integer
+ The EXPLICIT tag to use for the encoded object.
+ """
+ DerObject.__init__(self, 0x03, b'', implicit, False, explicit)
+
+ # The bitstring value (packed)
+ if isinstance(value, DerObject):
+ self.value = value.encode()
+ else:
+ self.value = value
+
+ def encode(self):
+ """Return the DER BIT STRING, fully encoded as a
+ byte string."""
+
+ # Add padding count byte
+ self.payload = b'\x00' + self.value
+ return DerObject.encode(self)
+
+ def decode(self, der_encoded, strict=False):
+ """Decode a complete DER BIT STRING, and re-initializes this
+ object with it.
+
+ Args:
+ der_encoded (byte string): a complete DER BIT STRING.
+ strict (boolean):
+ Whether decoding must check for strict DER compliancy.
+
+ Raises:
+ ValueError: in case of parsing errors.
+ """
+
+ return DerObject.decode(self, der_encoded, strict)
+
+ def _decodeFromStream(self, s, strict):
+ """Decode a complete DER BIT STRING DER from a file."""
+
+ # Fill-up self.payload
+ DerObject._decodeFromStream(self, s, strict)
+
+ if self.payload and bord(self.payload[0]) != 0:
+ raise ValueError("Not a valid BIT STRING")
+
+ # Fill-up self.value
+ self.value = b''
+ # Remove padding count byte
+ if self.payload:
+ self.value = self.payload[1:]
+
+
+class DerSetOf(DerObject):
+ """Class to model a DER SET OF.
+
+ An example of encoding is:
+
+ >>> from Crypto.Util.asn1 import DerBitString
+ >>> from binascii import hexlify, unhexlify
+ >>> so_der = DerSetOf([4,5])
+ >>> so_der.add(6)
+ >>> print hexlify(so_der.encode())
+
+ which will show ``3109020104020105020106``, the DER encoding
+ of a SET OF with items 4,5, and 6.
+
+ For decoding:
+
+ >>> s = unhexlify(b'3109020104020105020106')
+ >>> try:
+ >>> so_der = DerSetOf()
+ >>> so_der.decode(s)
+ >>> print [x for x in so_der]
+ >>> except ValueError:
+ >>> print "Not a valid DER SET OF"
+
+ the output will be ``[4, 5, 6]``.
+ """
+
+ def __init__(self, startSet=None, implicit=None):
+ """Initialize the DER object as a SET OF.
+
+ :Parameters:
+ startSet : container
+ The initial set of integers or DER encoded objects.
+ implicit : integer
+ The IMPLICIT tag to use for the encoded object.
+ It overrides the universal tag for SET OF (17).
+ """
+ DerObject.__init__(self, 0x11, b'', implicit, True)
+ self._seq = []
+
+ # All elements must be of the same type (and therefore have the
+ # same leading octet)
+ self._elemOctet = None
+
+ if startSet:
+ for e in startSet:
+ self.add(e)
+
+ def __getitem__(self, n):
+ return self._seq[n]
+
+ def __iter__(self):
+ return iter(self._seq)
+
+ def __len__(self):
+ return len(self._seq)
+
+ def add(self, elem):
+ """Add an element to the set.
+
+ Args:
+ elem (byte string or integer):
+ An element of the same type of objects already in the set.
+ It can be an integer or a DER encoded object.
+ """
+
+ if _is_number(elem):
+ eo = 0x02
+ elif isinstance(elem, DerObject):
+ eo = self._tag_octet
+ else:
+ eo = bord(elem[0])
+
+ if self._elemOctet != eo:
+ if self._elemOctet is not None:
+ raise ValueError("New element does not belong to the set")
+ self._elemOctet = eo
+
+ if elem not in self._seq:
+ self._seq.append(elem)
+
+ def decode(self, der_encoded, strict=False):
+ """Decode a complete SET OF DER element, and re-initializes this
+ object with it.
+
+ DER INTEGERs are decoded into Python integers. Any other DER
+ element is left undecoded; its validity is not checked.
+
+ Args:
+ der_encoded (byte string): a complete DER BIT SET OF.
+ strict (boolean):
+ Whether decoding must check for strict DER compliancy.
+
+ Raises:
+ ValueError: in case of parsing errors.
+ """
+
+ return DerObject.decode(self, der_encoded, strict)
+
+ def _decodeFromStream(self, s, strict):
+ """Decode a complete DER SET OF from a file."""
+
+ self._seq = []
+
+ # Fill up self.payload
+ DerObject._decodeFromStream(self, s, strict)
+
+ # Add one item at a time to self.seq, by scanning self.payload
+ p = BytesIO_EOF(self.payload)
+ setIdOctet = -1
+ while p.remaining_data() > 0:
+ p.set_bookmark()
+
+ der = DerObject()
+ der._decodeFromStream(p, strict)
+
+ # Verify that all members are of the same type
+ if setIdOctet < 0:
+ setIdOctet = der._tag_octet
+ else:
+ if setIdOctet != der._tag_octet:
+ raise ValueError("Not all elements are of the same DER type")
+
+ # Parse INTEGERs differently
+ if setIdOctet != 0x02:
+ self._seq.append(p.data_since_bookmark())
+ else:
+ derInt = DerInteger()
+ derInt.decode(p.data_since_bookmark(), strict)
+ self._seq.append(derInt.value)
+ # end
+
+ def encode(self):
+ """Return this SET OF DER element, fully encoded as a
+ binary string.
+ """
+
+ # Elements in the set must be ordered in lexicographic order
+ ordered = []
+ for item in self._seq:
+ if _is_number(item):
+ bys = DerInteger(item).encode()
+ elif isinstance(item, DerObject):
+ bys = item.encode()
+ else:
+ bys = item
+ ordered.append(bys)
+ ordered.sort()
+ self.payload = b''.join(ordered)
+ return DerObject.encode(self)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/asn1.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Util/asn1.pyi
new file mode 100644
index 00000000..ee4891c5
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/asn1.pyi
@@ -0,0 +1,80 @@
+from typing import Optional, Sequence, Union, Set, Iterable
+
+__all__ = ['DerObject', 'DerInteger', 'DerOctetString', 'DerNull',
+ 'DerSequence', 'DerObjectId', 'DerBitString', 'DerSetOf']
+
+# TODO: Make the encoded DerObjects their own type, so that DerSequence and
+# DerSetOf can check their contents better
+
+class BytesIO_EOF:
+ def __init__(self, initial_bytes: bytes) -> None: ...
+ def set_bookmark(self) -> None: ...
+ def data_since_bookmark(self) -> bytes: ...
+ def remaining_data(self) -> int: ...
+ def read(self, length: int) -> bytes: ...
+ def read_byte(self) -> bytes: ...
+
+class DerObject:
+ payload: bytes
+ def __init__(self, asn1Id: Optional[int]=None, payload: Optional[bytes]=..., implicit: Optional[int]=None,
+ constructed: Optional[bool]=False, explicit: Optional[int]=None) -> None: ...
+ def encode(self) -> bytes: ...
+ def decode(self, der_encoded: bytes, strict: bool=...) -> DerObject: ...
+
+class DerInteger(DerObject):
+ value: int
+ def __init__(self, value: Optional[int]= 0, implicit: Optional[int]=None, explicit: Optional[int]=None) -> None: ...
+ def encode(self) -> bytes: ...
+ def decode(self, der_encoded: bytes, strict: bool=...) -> DerInteger: ...
+
+class DerBoolean(DerObject):
+ value: bool
+ def __init__(self, value: bool=..., implicit: Optional[Union[int, bytes]]=..., explicit: Optional[Union[int, bytes]]=...) -> None: ...
+ def encode(self) -> bytes: ...
+ def decode(self, der_encoded: bytes, strict: bool=...) -> DerBoolean: ...
+
+class DerSequence(DerObject):
+ def __init__(self, startSeq: Optional[Sequence[Union[int, DerInteger, DerObject]]]=None, implicit: Optional[int]=None) -> None: ...
+ def __delitem__(self, n: int) -> None: ...
+ def __getitem__(self, n: int) -> None: ...
+ def __setitem__(self, key: int, value: DerObject) -> None: ...
+ def __setslice__(self, i: int, j: int, sequence: Sequence) -> None: ...
+ def __delslice__(self, i: int, j: int) -> None: ...
+ def __getslice__(self, i: int, j: int) -> DerSequence: ...
+ def __len__(self) -> int: ...
+ def __iadd__(self, item: DerObject) -> DerSequence: ...
+ def append(self, item: DerObject) -> DerSequence: ...
+ def hasInts(self, only_non_negative: Optional[bool]=True) -> int: ...
+ def hasOnlyInts(self, only_non_negative: Optional[bool]=True) -> bool: ...
+ def encode(self) -> bytes: ...
+ def decode(self, der_encoded: bytes, strict: bool=..., nr_elements: Optional[int]=None, only_ints_expected: Optional[bool]=False) -> DerSequence: ...
+
+class DerOctetString(DerObject):
+ payload: bytes
+ def __init__(self, value: Optional[bytes]=..., implicit: Optional[int]=None) -> None: ...
+
+class DerNull(DerObject):
+ def __init__(self) -> None: ...
+
+class DerObjectId(DerObject):
+ value: str
+ def __init__(self, value: Optional[str]=..., implicit: Optional[int]=None, explicit: Optional[int]=None) -> None: ...
+ def encode(self) -> bytes: ...
+ def decode(self, der_encoded: bytes, strict: bool=...) -> DerObjectId: ...
+
+class DerBitString(DerObject):
+ value: bytes
+ def __init__(self, value: Optional[bytes]=..., implicit: Optional[int]=None, explicit: Optional[int]=None) -> None: ...
+ def encode(self) -> bytes: ...
+ def decode(self, der_encoded: bytes, strict: bool=...) -> DerBitString: ...
+
+DerSetElement = Union[bytes, int]
+
+class DerSetOf(DerObject):
+ def __init__(self, startSet: Optional[Set[DerSetElement]]=None, implicit: Optional[int]=None) -> None: ...
+ def __getitem__(self, n: int) -> DerSetElement: ...
+ def __iter__(self) -> Iterable: ...
+ def __len__(self) -> int: ...
+ def add(self, elem: DerSetElement) -> None: ...
+ def decode(self, der_encoded: bytes, strict: bool=...) -> DerObject: ...
+ def encode(self) -> bytes: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/number.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/number.py
new file mode 100644
index 00000000..a1c0d5c7
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/number.py
@@ -0,0 +1,1525 @@
+#
+# number.py : Number-theoretic functions
+#
+# Part of the Python Cryptography Toolkit
+#
+# Written by Andrew M. Kuchling, Barry A. Warsaw, and others
+#
+# ===================================================================
+# 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 math
+import sys
+import struct
+from Crypto import Random
+from Crypto.Util.py3compat import iter_range
+
+# Backward compatibility
+_fastmath = None
+
+
+def ceil_div(n, d):
+ """Return ceil(n/d), that is, the smallest integer r such that r*d >= n"""
+
+ if d == 0:
+ raise ZeroDivisionError()
+ if (n < 0) or (d < 0):
+ raise ValueError("Non positive values")
+ r, q = divmod(n, d)
+ if (n != 0) and (q != 0):
+ r += 1
+ return r
+
+
+def size (N):
+ """Returns the size of the number N in bits."""
+
+ if N < 0:
+ raise ValueError("Size in bits only available for non-negative numbers")
+ return N.bit_length()
+
+
+def getRandomInteger(N, randfunc=None):
+ """Return a random number at most N bits long.
+
+ If :data:`randfunc` is omitted, then :meth:`Random.get_random_bytes` is used.
+
+ .. deprecated:: 3.0
+ This function is for internal use only and may be renamed or removed in
+ the future. Use :func:`Crypto.Random.random.getrandbits` instead.
+ """
+
+ if randfunc is None:
+ randfunc = Random.get_random_bytes
+
+ S = randfunc(N>>3)
+ odd_bits = N % 8
+ if odd_bits != 0:
+ rand_bits = ord(randfunc(1)) >> (8-odd_bits)
+ S = struct.pack('B', rand_bits) + S
+ value = bytes_to_long(S)
+ return value
+
+def getRandomRange(a, b, randfunc=None):
+ """Return a random number *n* so that *a <= n < b*.
+
+ If :data:`randfunc` is omitted, then :meth:`Random.get_random_bytes` is used.
+
+ .. deprecated:: 3.0
+ This function is for internal use only and may be renamed or removed in
+ the future. Use :func:`Crypto.Random.random.randrange` instead.
+ """
+
+ range_ = b - a - 1
+ bits = size(range_)
+ value = getRandomInteger(bits, randfunc)
+ while value > range_:
+ value = getRandomInteger(bits, randfunc)
+ return a + value
+
+def getRandomNBitInteger(N, randfunc=None):
+ """Return a random number with exactly N-bits,
+ i.e. a random number between 2**(N-1) and (2**N)-1.
+
+ If :data:`randfunc` is omitted, then :meth:`Random.get_random_bytes` is used.
+
+ .. deprecated:: 3.0
+ This function is for internal use only and may be renamed or removed in
+ the future.
+ """
+
+ value = getRandomInteger (N-1, randfunc)
+ value |= 2 ** (N-1) # Ensure high bit is set
+ assert size(value) >= N
+ return value
+
+
+if sys.version_info[:2] >= (3, 5):
+
+ GCD = math.gcd
+
+else:
+
+ def GCD(x,y):
+ """Greatest Common Denominator of :data:`x` and :data:`y`.
+ """
+
+ x = abs(x) ; y = abs(y)
+ while x > 0:
+ x, y = y % x, x
+ return y
+
+
+if sys.version_info[:2] >= (3, 8):
+
+ def inverse(u, v):
+ """The inverse of :data:`u` *mod* :data:`v`."""
+
+ if v == 0:
+ raise ZeroDivisionError("Modulus cannot be zero")
+ if v < 0:
+ raise ValueError("Modulus cannot be negative")
+
+ return pow(u, -1, v)
+
+else:
+
+ def inverse(u, v):
+ """The inverse of :data:`u` *mod* :data:`v`."""
+
+ if v == 0:
+ raise ZeroDivisionError("Modulus cannot be zero")
+ if v < 0:
+ raise ValueError("Modulus cannot be negative")
+
+ u3, v3 = u, v
+ u1, v1 = 1, 0
+ while v3 > 0:
+ q = u3 // v3
+ u1, v1 = v1, u1 - v1*q
+ u3, v3 = v3, u3 - v3*q
+ if u3 != 1:
+ raise ValueError("No inverse value can be computed")
+ while u1<0:
+ u1 = u1 + v
+ return u1
+
+# Given a number of bits to generate and a random generation function,
+# find a prime number of the appropriate size.
+
+def getPrime(N, randfunc=None):
+ """Return a random N-bit prime number.
+
+ N must be an integer larger than 1.
+ If randfunc is omitted, then :meth:`Random.get_random_bytes` is used.
+ """
+ if randfunc is None:
+ randfunc = Random.get_random_bytes
+
+ if N < 2:
+ raise ValueError("N must be larger than 1")
+
+ while True:
+ number = getRandomNBitInteger(N, randfunc) | 1
+ if isPrime(number, randfunc=randfunc):
+ break
+ return number
+
+
+def _rabinMillerTest(n, rounds, randfunc=None):
+ """_rabinMillerTest(n:long, rounds:int, randfunc:callable):int
+ Tests if n is prime.
+ Returns 0 when n is definitely composite.
+ Returns 1 when n is probably prime.
+ Returns 2 when n is definitely prime.
+
+ If randfunc is omitted, then Random.new().read is used.
+
+ This function is for internal use only and may be renamed or removed in
+ the future.
+ """
+ # check special cases (n==2, n even, n < 2)
+ if n < 3 or (n & 1) == 0:
+ return n == 2
+ # n might be very large so it might be beneficial to precalculate n-1
+ n_1 = n - 1
+ # determine m and b so that 2**b * m = n - 1 and b maximal
+ b = 0
+ m = n_1
+ while (m & 1) == 0:
+ b += 1
+ m >>= 1
+
+ tested = []
+ # we need to do at most n-2 rounds.
+ for i in iter_range (min (rounds, n-2)):
+ # randomly choose a < n and make sure it hasn't been tested yet
+ a = getRandomRange (2, n, randfunc)
+ while a in tested:
+ a = getRandomRange (2, n, randfunc)
+ tested.append (a)
+ # do the rabin-miller test
+ z = pow (a, m, n) # (a**m) % n
+ if z == 1 or z == n_1:
+ continue
+ composite = 1
+ for r in iter_range(b):
+ z = (z * z) % n
+ if z == 1:
+ return 0
+ elif z == n_1:
+ composite = 0
+ break
+ if composite:
+ return 0
+ return 1
+
+def getStrongPrime(N, e=0, false_positive_prob=1e-6, randfunc=None):
+ r"""
+ Return a random strong *N*-bit prime number.
+ In this context, *p* is a strong prime if *p-1* and *p+1* have at
+ least one large prime factor.
+
+ Args:
+ N (integer): the exact length of the strong prime.
+ It must be a multiple of 128 and > 512.
+ e (integer): if provided, the returned prime (minus 1)
+ will be coprime to *e* and thus suitable for RSA where
+ *e* is the public exponent.
+ false_positive_prob (float):
+ The statistical probability for the result not to be actually a
+ prime. It defaults to 10\ :sup:`-6`.
+ Note that the real probability of a false-positive is far less. This is
+ just the mathematically provable limit.
+ randfunc (callable):
+ A function that takes a parameter *N* and that returns
+ a random byte string of such length.
+ If omitted, :func:`Crypto.Random.get_random_bytes` is used.
+ Return:
+ The new strong prime.
+
+ .. deprecated:: 3.0
+ This function is for internal use only and may be renamed or removed in
+ the future.
+ """
+
+ # This function was implemented following the
+ # instructions found in the paper:
+ # "FAST GENERATION OF RANDOM, STRONG RSA PRIMES"
+ # by Robert D. Silverman
+ # RSA Laboratories
+ # May 17, 1997
+ # which by the time of writing could be freely downloaded here:
+ # http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.17.2713&rep=rep1&type=pdf
+
+ if randfunc is None:
+ randfunc = Random.get_random_bytes
+
+ # Use the accelerator if available
+ if _fastmath is not None:
+ return _fastmath.getStrongPrime(long(N), long(e), false_positive_prob,
+ randfunc)
+
+ if (N < 512) or ((N % 128) != 0):
+ raise ValueError ("bits must be multiple of 128 and > 512")
+
+ rabin_miller_rounds = int(math.ceil(-math.log(false_positive_prob)/math.log(4)))
+
+ # calculate range for X
+ # lower_bound = sqrt(2) * 2^{511 + 128*x}
+ # upper_bound = 2^{512 + 128*x} - 1
+ x = (N - 512) >> 7
+ # We need to approximate the sqrt(2) in the lower_bound by an integer
+ # expression because floating point math overflows with these numbers
+ lower_bound = (14142135623730950489 * (2 ** (511 + 128*x))) // 10000000000000000000
+ upper_bound = (1 << (512 + 128*x)) - 1
+ # Randomly choose X in calculated range
+ X = getRandomRange (lower_bound, upper_bound, randfunc)
+
+ # generate p1 and p2
+ p = [0, 0]
+ for i in (0, 1):
+ # randomly choose 101-bit y
+ y = getRandomNBitInteger (101, randfunc)
+ # initialize the field for sieving
+ field = [0] * 5 * len (sieve_base)
+ # sieve the field
+ for prime in sieve_base:
+ offset = y % prime
+ for j in iter_range((prime - offset) % prime, len (field), prime):
+ field[j] = 1
+
+ # look for suitable p[i] starting at y
+ result = 0
+ for j in range(len(field)):
+ composite = field[j]
+ # look for next canidate
+ if composite:
+ continue
+ tmp = y + j
+ result = _rabinMillerTest (tmp, rabin_miller_rounds)
+ if result > 0:
+ p[i] = tmp
+ break
+ if result == 0:
+ raise RuntimeError ("Couln't find prime in field. "
+ "Developer: Increase field_size")
+
+ # Calculate R
+ # R = (p2^{-1} mod p1) * p2 - (p1^{-1} mod p2) * p1
+ tmp1 = inverse (p[1], p[0]) * p[1] # (p2^-1 mod p1)*p2
+ tmp2 = inverse (p[0], p[1]) * p[0] # (p1^-1 mod p2)*p1
+ R = tmp1 - tmp2 # (p2^-1 mod p1)*p2 - (p1^-1 mod p2)*p1
+
+ # search for final prime number starting by Y0
+ # Y0 = X + (R - X mod p1p2)
+ increment = p[0] * p[1]
+ X = X + (R - (X % increment))
+ while 1:
+ is_possible_prime = 1
+ # first check candidate against sieve_base
+ for prime in sieve_base:
+ if (X % prime) == 0:
+ is_possible_prime = 0
+ break
+ # if e is given make sure that e and X-1 are coprime
+ # this is not necessarily a strong prime criterion but useful when
+ # creating them for RSA where the p-1 and q-1 should be coprime to
+ # the public exponent e
+ if e and is_possible_prime:
+ if e & 1:
+ if GCD(e, X-1) != 1:
+ is_possible_prime = 0
+ else:
+ if GCD(e, (X-1) // 2) != 1:
+ is_possible_prime = 0
+
+ # do some Rabin-Miller-Tests
+ if is_possible_prime:
+ result = _rabinMillerTest (X, rabin_miller_rounds)
+ if result > 0:
+ break
+ X += increment
+ # abort when X has more bits than requested
+ # TODO: maybe we shouldn't abort but rather start over.
+ if X >= 1 << N:
+ raise RuntimeError ("Couln't find prime in field. "
+ "Developer: Increase field_size")
+ return X
+
+def isPrime(N, false_positive_prob=1e-6, randfunc=None):
+ r"""Test if a number *N* is a prime.
+
+ Args:
+ false_positive_prob (float):
+ The statistical probability for the result not to be actually a
+ prime. It defaults to 10\ :sup:`-6`.
+ Note that the real probability of a false-positive is far less.
+ This is just the mathematically provable limit.
+ randfunc (callable):
+ A function that takes a parameter *N* and that returns
+ a random byte string of such length.
+ If omitted, :func:`Crypto.Random.get_random_bytes` is used.
+
+ Return:
+ `True` if the input is indeed prime.
+ """
+
+ if randfunc is None:
+ randfunc = Random.get_random_bytes
+
+ if _fastmath is not None:
+ return _fastmath.isPrime(long(N), false_positive_prob, randfunc)
+
+ if N < 3 or N & 1 == 0:
+ return N == 2
+ for p in sieve_base:
+ if N == p:
+ return True
+ if N % p == 0:
+ return False
+
+ rounds = int(math.ceil(-math.log(false_positive_prob)/math.log(4)))
+ return bool(_rabinMillerTest(N, rounds, randfunc))
+
+
+# Improved conversion functions contributed by Barry Warsaw, after
+# careful benchmarking
+
+import struct
+
+def long_to_bytes(n, blocksize=0):
+ """Convert a positive integer to a byte string using big endian encoding.
+
+ If :data:`blocksize` is absent or zero, the byte string will
+ be of minimal length.
+
+ Otherwise, the length of the byte string is guaranteed to be a multiple
+ of :data:`blocksize`. If necessary, zeroes (``\\x00``) are added at the left.
+
+ .. note::
+ In Python 3, if you are sure that :data:`n` can fit into
+ :data:`blocksize` bytes, you can simply use the native method instead::
+
+ >>> n.to_bytes(blocksize, 'big')
+
+ For instance::
+
+ >>> n = 80
+ >>> n.to_bytes(2, 'big')
+ b'\\x00P'
+
+ However, and unlike this ``long_to_bytes()`` function,
+ an ``OverflowError`` exception is raised if :data:`n` does not fit.
+ """
+
+ if n < 0 or blocksize < 0:
+ raise ValueError("Values must be non-negative")
+
+ result = []
+ pack = struct.pack
+
+ # Fill the first block independently from the value of n
+ bsr = blocksize
+ while bsr >= 8:
+ result.insert(0, pack('>Q', n & 0xFFFFFFFFFFFFFFFF))
+ n = n >> 64
+ bsr -= 8
+
+ while bsr >= 4:
+ result.insert(0, pack('>I', n & 0xFFFFFFFF))
+ n = n >> 32
+ bsr -= 4
+
+ while bsr > 0:
+ result.insert(0, pack('>B', n & 0xFF))
+ n = n >> 8
+ bsr -= 1
+
+ if n == 0:
+ if len(result) == 0:
+ bresult = b'\x00'
+ else:
+ bresult = b''.join(result)
+ else:
+ # The encoded number exceeds the block size
+ while n > 0:
+ result.insert(0, pack('>Q', n & 0xFFFFFFFFFFFFFFFF))
+ n = n >> 64
+ result[0] = result[0].lstrip(b'\x00')
+ bresult = b''.join(result)
+ # bresult has minimum length here
+ if blocksize > 0:
+ target_len = ((len(bresult) - 1) // blocksize + 1) * blocksize
+ bresult = b'\x00' * (target_len - len(bresult)) + bresult
+
+ return bresult
+
+
+def bytes_to_long(s):
+ """Convert a byte string to a long integer (big endian).
+
+ In Python 3.2+, use the native method instead::
+
+ >>> int.from_bytes(s, 'big')
+
+ For instance::
+
+ >>> int.from_bytes(b'\x00P', 'big')
+ 80
+
+ This is (essentially) the inverse of :func:`long_to_bytes`.
+ """
+ acc = 0
+
+ unpack = struct.unpack
+
+ # Up to Python 2.7.4, struct.unpack can't work with bytearrays nor
+ # memoryviews
+ if sys.version_info[0:3] < (2, 7, 4):
+ if isinstance(s, bytearray):
+ s = bytes(s)
+ elif isinstance(s, memoryview):
+ s = s.tobytes()
+
+ length = len(s)
+ if length % 4:
+ extra = (4 - length % 4)
+ s = b'\x00' * extra + s
+ length = length + extra
+ for i in range(0, length, 4):
+ acc = (acc << 32) + unpack('>I', s[i:i+4])[0]
+ return acc
+
+
+# For backwards compatibility...
+import warnings
+def long2str(n, blocksize=0):
+ warnings.warn("long2str() has been replaced by long_to_bytes()")
+ return long_to_bytes(n, blocksize)
+def str2long(s):
+ warnings.warn("str2long() has been replaced by bytes_to_long()")
+ return bytes_to_long(s)
+
+
+# The first 10000 primes used for checking primality.
+# This should be enough to eliminate most of the odd
+# numbers before needing to do a Rabin-Miller test at all.
+sieve_base = (
+ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
+ 31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
+ 73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
+ 127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
+ 179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
+ 233, 239, 241, 251, 257, 263, 269, 271, 277, 281,
+ 283, 293, 307, 311, 313, 317, 331, 337, 347, 349,
+ 353, 359, 367, 373, 379, 383, 389, 397, 401, 409,
+ 419, 421, 431, 433, 439, 443, 449, 457, 461, 463,
+ 467, 479, 487, 491, 499, 503, 509, 521, 523, 541,
+ 547, 557, 563, 569, 571, 577, 587, 593, 599, 601,
+ 607, 613, 617, 619, 631, 641, 643, 647, 653, 659,
+ 661, 673, 677, 683, 691, 701, 709, 719, 727, 733,
+ 739, 743, 751, 757, 761, 769, 773, 787, 797, 809,
+ 811, 821, 823, 827, 829, 839, 853, 857, 859, 863,
+ 877, 881, 883, 887, 907, 911, 919, 929, 937, 941,
+ 947, 953, 967, 971, 977, 983, 991, 997, 1009, 1013,
+ 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063, 1069,
+ 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151,
+ 1153, 1163, 1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223,
+ 1229, 1231, 1237, 1249, 1259, 1277, 1279, 1283, 1289, 1291,
+ 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373,
+ 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451,
+ 1453, 1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511,
+ 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583,
+ 1597, 1601, 1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657,
+ 1663, 1667, 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733,
+ 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811,
+ 1823, 1831, 1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889,
+ 1901, 1907, 1913, 1931, 1933, 1949, 1951, 1973, 1979, 1987,
+ 1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053,
+ 2063, 2069, 2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129,
+ 2131, 2137, 2141, 2143, 2153, 2161, 2179, 2203, 2207, 2213,
+ 2221, 2237, 2239, 2243, 2251, 2267, 2269, 2273, 2281, 2287,
+ 2293, 2297, 2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357,
+ 2371, 2377, 2381, 2383, 2389, 2393, 2399, 2411, 2417, 2423,
+ 2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531,
+ 2539, 2543, 2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617,
+ 2621, 2633, 2647, 2657, 2659, 2663, 2671, 2677, 2683, 2687,
+ 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741,
+ 2749, 2753, 2767, 2777, 2789, 2791, 2797, 2801, 2803, 2819,
+ 2833, 2837, 2843, 2851, 2857, 2861, 2879, 2887, 2897, 2903,
+ 2909, 2917, 2927, 2939, 2953, 2957, 2963, 2969, 2971, 2999,
+ 3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, 3067, 3079,
+ 3083, 3089, 3109, 3119, 3121, 3137, 3163, 3167, 3169, 3181,
+ 3187, 3191, 3203, 3209, 3217, 3221, 3229, 3251, 3253, 3257,
+ 3259, 3271, 3299, 3301, 3307, 3313, 3319, 3323, 3329, 3331,
+ 3343, 3347, 3359, 3361, 3371, 3373, 3389, 3391, 3407, 3413,
+ 3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, 3499, 3511,
+ 3517, 3527, 3529, 3533, 3539, 3541, 3547, 3557, 3559, 3571,
+ 3581, 3583, 3593, 3607, 3613, 3617, 3623, 3631, 3637, 3643,
+ 3659, 3671, 3673, 3677, 3691, 3697, 3701, 3709, 3719, 3727,
+ 3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797, 3803, 3821,
+ 3823, 3833, 3847, 3851, 3853, 3863, 3877, 3881, 3889, 3907,
+ 3911, 3917, 3919, 3923, 3929, 3931, 3943, 3947, 3967, 3989,
+ 4001, 4003, 4007, 4013, 4019, 4021, 4027, 4049, 4051, 4057,
+ 4073, 4079, 4091, 4093, 4099, 4111, 4127, 4129, 4133, 4139,
+ 4153, 4157, 4159, 4177, 4201, 4211, 4217, 4219, 4229, 4231,
+ 4241, 4243, 4253, 4259, 4261, 4271, 4273, 4283, 4289, 4297,
+ 4327, 4337, 4339, 4349, 4357, 4363, 4373, 4391, 4397, 4409,
+ 4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481, 4483, 4493,
+ 4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583,
+ 4591, 4597, 4603, 4621, 4637, 4639, 4643, 4649, 4651, 4657,
+ 4663, 4673, 4679, 4691, 4703, 4721, 4723, 4729, 4733, 4751,
+ 4759, 4783, 4787, 4789, 4793, 4799, 4801, 4813, 4817, 4831,
+ 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937,
+ 4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999, 5003,
+ 5009, 5011, 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087,
+ 5099, 5101, 5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179,
+ 5189, 5197, 5209, 5227, 5231, 5233, 5237, 5261, 5273, 5279,
+ 5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351, 5381, 5387,
+ 5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443,
+ 5449, 5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519, 5521,
+ 5527, 5531, 5557, 5563, 5569, 5573, 5581, 5591, 5623, 5639,
+ 5641, 5647, 5651, 5653, 5657, 5659, 5669, 5683, 5689, 5693,
+ 5701, 5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791,
+ 5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857,
+ 5861, 5867, 5869, 5879, 5881, 5897, 5903, 5923, 5927, 5939,
+ 5953, 5981, 5987, 6007, 6011, 6029, 6037, 6043, 6047, 6053,
+ 6067, 6073, 6079, 6089, 6091, 6101, 6113, 6121, 6131, 6133,
+ 6143, 6151, 6163, 6173, 6197, 6199, 6203, 6211, 6217, 6221,
+ 6229, 6247, 6257, 6263, 6269, 6271, 6277, 6287, 6299, 6301,
+ 6311, 6317, 6323, 6329, 6337, 6343, 6353, 6359, 6361, 6367,
+ 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451, 6469, 6473,
+ 6481, 6491, 6521, 6529, 6547, 6551, 6553, 6563, 6569, 6571,
+ 6577, 6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673,
+ 6679, 6689, 6691, 6701, 6703, 6709, 6719, 6733, 6737, 6761,
+ 6763, 6779, 6781, 6791, 6793, 6803, 6823, 6827, 6829, 6833,
+ 6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907, 6911, 6917,
+ 6947, 6949, 6959, 6961, 6967, 6971, 6977, 6983, 6991, 6997,
+ 7001, 7013, 7019, 7027, 7039, 7043, 7057, 7069, 7079, 7103,
+ 7109, 7121, 7127, 7129, 7151, 7159, 7177, 7187, 7193, 7207,
+ 7211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, 7283, 7297,
+ 7307, 7309, 7321, 7331, 7333, 7349, 7351, 7369, 7393, 7411,
+ 7417, 7433, 7451, 7457, 7459, 7477, 7481, 7487, 7489, 7499,
+ 7507, 7517, 7523, 7529, 7537, 7541, 7547, 7549, 7559, 7561,
+ 7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621, 7639, 7643,
+ 7649, 7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723,
+ 7727, 7741, 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829,
+ 7841, 7853, 7867, 7873, 7877, 7879, 7883, 7901, 7907, 7919,
+ 7927, 7933, 7937, 7949, 7951, 7963, 7993, 8009, 8011, 8017,
+ 8039, 8053, 8059, 8069, 8081, 8087, 8089, 8093, 8101, 8111,
+ 8117, 8123, 8147, 8161, 8167, 8171, 8179, 8191, 8209, 8219,
+ 8221, 8231, 8233, 8237, 8243, 8263, 8269, 8273, 8287, 8291,
+ 8293, 8297, 8311, 8317, 8329, 8353, 8363, 8369, 8377, 8387,
+ 8389, 8419, 8423, 8429, 8431, 8443, 8447, 8461, 8467, 8501,
+ 8513, 8521, 8527, 8537, 8539, 8543, 8563, 8573, 8581, 8597,
+ 8599, 8609, 8623, 8627, 8629, 8641, 8647, 8663, 8669, 8677,
+ 8681, 8689, 8693, 8699, 8707, 8713, 8719, 8731, 8737, 8741,
+ 8747, 8753, 8761, 8779, 8783, 8803, 8807, 8819, 8821, 8831,
+ 8837, 8839, 8849, 8861, 8863, 8867, 8887, 8893, 8923, 8929,
+ 8933, 8941, 8951, 8963, 8969, 8971, 8999, 9001, 9007, 9011,
+ 9013, 9029, 9041, 9043, 9049, 9059, 9067, 9091, 9103, 9109,
+ 9127, 9133, 9137, 9151, 9157, 9161, 9173, 9181, 9187, 9199,
+ 9203, 9209, 9221, 9227, 9239, 9241, 9257, 9277, 9281, 9283,
+ 9293, 9311, 9319, 9323, 9337, 9341, 9343, 9349, 9371, 9377,
+ 9391, 9397, 9403, 9413, 9419, 9421, 9431, 9433, 9437, 9439,
+ 9461, 9463, 9467, 9473, 9479, 9491, 9497, 9511, 9521, 9533,
+ 9539, 9547, 9551, 9587, 9601, 9613, 9619, 9623, 9629, 9631,
+ 9643, 9649, 9661, 9677, 9679, 9689, 9697, 9719, 9721, 9733,
+ 9739, 9743, 9749, 9767, 9769, 9781, 9787, 9791, 9803, 9811,
+ 9817, 9829, 9833, 9839, 9851, 9857, 9859, 9871, 9883, 9887,
+ 9901, 9907, 9923, 9929, 9931, 9941, 9949, 9967, 9973, 10007,
+ 10009, 10037, 10039, 10061, 10067, 10069, 10079, 10091, 10093, 10099,
+ 10103, 10111, 10133, 10139, 10141, 10151, 10159, 10163, 10169, 10177,
+ 10181, 10193, 10211, 10223, 10243, 10247, 10253, 10259, 10267, 10271,
+ 10273, 10289, 10301, 10303, 10313, 10321, 10331, 10333, 10337, 10343,
+ 10357, 10369, 10391, 10399, 10427, 10429, 10433, 10453, 10457, 10459,
+ 10463, 10477, 10487, 10499, 10501, 10513, 10529, 10531, 10559, 10567,
+ 10589, 10597, 10601, 10607, 10613, 10627, 10631, 10639, 10651, 10657,
+ 10663, 10667, 10687, 10691, 10709, 10711, 10723, 10729, 10733, 10739,
+ 10753, 10771, 10781, 10789, 10799, 10831, 10837, 10847, 10853, 10859,
+ 10861, 10867, 10883, 10889, 10891, 10903, 10909, 10937, 10939, 10949,
+ 10957, 10973, 10979, 10987, 10993, 11003, 11027, 11047, 11057, 11059,
+ 11069, 11071, 11083, 11087, 11093, 11113, 11117, 11119, 11131, 11149,
+ 11159, 11161, 11171, 11173, 11177, 11197, 11213, 11239, 11243, 11251,
+ 11257, 11261, 11273, 11279, 11287, 11299, 11311, 11317, 11321, 11329,
+ 11351, 11353, 11369, 11383, 11393, 11399, 11411, 11423, 11437, 11443,
+ 11447, 11467, 11471, 11483, 11489, 11491, 11497, 11503, 11519, 11527,
+ 11549, 11551, 11579, 11587, 11593, 11597, 11617, 11621, 11633, 11657,
+ 11677, 11681, 11689, 11699, 11701, 11717, 11719, 11731, 11743, 11777,
+ 11779, 11783, 11789, 11801, 11807, 11813, 11821, 11827, 11831, 11833,
+ 11839, 11863, 11867, 11887, 11897, 11903, 11909, 11923, 11927, 11933,
+ 11939, 11941, 11953, 11959, 11969, 11971, 11981, 11987, 12007, 12011,
+ 12037, 12041, 12043, 12049, 12071, 12073, 12097, 12101, 12107, 12109,
+ 12113, 12119, 12143, 12149, 12157, 12161, 12163, 12197, 12203, 12211,
+ 12227, 12239, 12241, 12251, 12253, 12263, 12269, 12277, 12281, 12289,
+ 12301, 12323, 12329, 12343, 12347, 12373, 12377, 12379, 12391, 12401,
+ 12409, 12413, 12421, 12433, 12437, 12451, 12457, 12473, 12479, 12487,
+ 12491, 12497, 12503, 12511, 12517, 12527, 12539, 12541, 12547, 12553,
+ 12569, 12577, 12583, 12589, 12601, 12611, 12613, 12619, 12637, 12641,
+ 12647, 12653, 12659, 12671, 12689, 12697, 12703, 12713, 12721, 12739,
+ 12743, 12757, 12763, 12781, 12791, 12799, 12809, 12821, 12823, 12829,
+ 12841, 12853, 12889, 12893, 12899, 12907, 12911, 12917, 12919, 12923,
+ 12941, 12953, 12959, 12967, 12973, 12979, 12983, 13001, 13003, 13007,
+ 13009, 13033, 13037, 13043, 13049, 13063, 13093, 13099, 13103, 13109,
+ 13121, 13127, 13147, 13151, 13159, 13163, 13171, 13177, 13183, 13187,
+ 13217, 13219, 13229, 13241, 13249, 13259, 13267, 13291, 13297, 13309,
+ 13313, 13327, 13331, 13337, 13339, 13367, 13381, 13397, 13399, 13411,
+ 13417, 13421, 13441, 13451, 13457, 13463, 13469, 13477, 13487, 13499,
+ 13513, 13523, 13537, 13553, 13567, 13577, 13591, 13597, 13613, 13619,
+ 13627, 13633, 13649, 13669, 13679, 13681, 13687, 13691, 13693, 13697,
+ 13709, 13711, 13721, 13723, 13729, 13751, 13757, 13759, 13763, 13781,
+ 13789, 13799, 13807, 13829, 13831, 13841, 13859, 13873, 13877, 13879,
+ 13883, 13901, 13903, 13907, 13913, 13921, 13931, 13933, 13963, 13967,
+ 13997, 13999, 14009, 14011, 14029, 14033, 14051, 14057, 14071, 14081,
+ 14083, 14087, 14107, 14143, 14149, 14153, 14159, 14173, 14177, 14197,
+ 14207, 14221, 14243, 14249, 14251, 14281, 14293, 14303, 14321, 14323,
+ 14327, 14341, 14347, 14369, 14387, 14389, 14401, 14407, 14411, 14419,
+ 14423, 14431, 14437, 14447, 14449, 14461, 14479, 14489, 14503, 14519,
+ 14533, 14537, 14543, 14549, 14551, 14557, 14561, 14563, 14591, 14593,
+ 14621, 14627, 14629, 14633, 14639, 14653, 14657, 14669, 14683, 14699,
+ 14713, 14717, 14723, 14731, 14737, 14741, 14747, 14753, 14759, 14767,
+ 14771, 14779, 14783, 14797, 14813, 14821, 14827, 14831, 14843, 14851,
+ 14867, 14869, 14879, 14887, 14891, 14897, 14923, 14929, 14939, 14947,
+ 14951, 14957, 14969, 14983, 15013, 15017, 15031, 15053, 15061, 15073,
+ 15077, 15083, 15091, 15101, 15107, 15121, 15131, 15137, 15139, 15149,
+ 15161, 15173, 15187, 15193, 15199, 15217, 15227, 15233, 15241, 15259,
+ 15263, 15269, 15271, 15277, 15287, 15289, 15299, 15307, 15313, 15319,
+ 15329, 15331, 15349, 15359, 15361, 15373, 15377, 15383, 15391, 15401,
+ 15413, 15427, 15439, 15443, 15451, 15461, 15467, 15473, 15493, 15497,
+ 15511, 15527, 15541, 15551, 15559, 15569, 15581, 15583, 15601, 15607,
+ 15619, 15629, 15641, 15643, 15647, 15649, 15661, 15667, 15671, 15679,
+ 15683, 15727, 15731, 15733, 15737, 15739, 15749, 15761, 15767, 15773,
+ 15787, 15791, 15797, 15803, 15809, 15817, 15823, 15859, 15877, 15881,
+ 15887, 15889, 15901, 15907, 15913, 15919, 15923, 15937, 15959, 15971,
+ 15973, 15991, 16001, 16007, 16033, 16057, 16061, 16063, 16067, 16069,
+ 16073, 16087, 16091, 16097, 16103, 16111, 16127, 16139, 16141, 16183,
+ 16187, 16189, 16193, 16217, 16223, 16229, 16231, 16249, 16253, 16267,
+ 16273, 16301, 16319, 16333, 16339, 16349, 16361, 16363, 16369, 16381,
+ 16411, 16417, 16421, 16427, 16433, 16447, 16451, 16453, 16477, 16481,
+ 16487, 16493, 16519, 16529, 16547, 16553, 16561, 16567, 16573, 16603,
+ 16607, 16619, 16631, 16633, 16649, 16651, 16657, 16661, 16673, 16691,
+ 16693, 16699, 16703, 16729, 16741, 16747, 16759, 16763, 16787, 16811,
+ 16823, 16829, 16831, 16843, 16871, 16879, 16883, 16889, 16901, 16903,
+ 16921, 16927, 16931, 16937, 16943, 16963, 16979, 16981, 16987, 16993,
+ 17011, 17021, 17027, 17029, 17033, 17041, 17047, 17053, 17077, 17093,
+ 17099, 17107, 17117, 17123, 17137, 17159, 17167, 17183, 17189, 17191,
+ 17203, 17207, 17209, 17231, 17239, 17257, 17291, 17293, 17299, 17317,
+ 17321, 17327, 17333, 17341, 17351, 17359, 17377, 17383, 17387, 17389,
+ 17393, 17401, 17417, 17419, 17431, 17443, 17449, 17467, 17471, 17477,
+ 17483, 17489, 17491, 17497, 17509, 17519, 17539, 17551, 17569, 17573,
+ 17579, 17581, 17597, 17599, 17609, 17623, 17627, 17657, 17659, 17669,
+ 17681, 17683, 17707, 17713, 17729, 17737, 17747, 17749, 17761, 17783,
+ 17789, 17791, 17807, 17827, 17837, 17839, 17851, 17863, 17881, 17891,
+ 17903, 17909, 17911, 17921, 17923, 17929, 17939, 17957, 17959, 17971,
+ 17977, 17981, 17987, 17989, 18013, 18041, 18043, 18047, 18049, 18059,
+ 18061, 18077, 18089, 18097, 18119, 18121, 18127, 18131, 18133, 18143,
+ 18149, 18169, 18181, 18191, 18199, 18211, 18217, 18223, 18229, 18233,
+ 18251, 18253, 18257, 18269, 18287, 18289, 18301, 18307, 18311, 18313,
+ 18329, 18341, 18353, 18367, 18371, 18379, 18397, 18401, 18413, 18427,
+ 18433, 18439, 18443, 18451, 18457, 18461, 18481, 18493, 18503, 18517,
+ 18521, 18523, 18539, 18541, 18553, 18583, 18587, 18593, 18617, 18637,
+ 18661, 18671, 18679, 18691, 18701, 18713, 18719, 18731, 18743, 18749,
+ 18757, 18773, 18787, 18793, 18797, 18803, 18839, 18859, 18869, 18899,
+ 18911, 18913, 18917, 18919, 18947, 18959, 18973, 18979, 19001, 19009,
+ 19013, 19031, 19037, 19051, 19069, 19073, 19079, 19081, 19087, 19121,
+ 19139, 19141, 19157, 19163, 19181, 19183, 19207, 19211, 19213, 19219,
+ 19231, 19237, 19249, 19259, 19267, 19273, 19289, 19301, 19309, 19319,
+ 19333, 19373, 19379, 19381, 19387, 19391, 19403, 19417, 19421, 19423,
+ 19427, 19429, 19433, 19441, 19447, 19457, 19463, 19469, 19471, 19477,
+ 19483, 19489, 19501, 19507, 19531, 19541, 19543, 19553, 19559, 19571,
+ 19577, 19583, 19597, 19603, 19609, 19661, 19681, 19687, 19697, 19699,
+ 19709, 19717, 19727, 19739, 19751, 19753, 19759, 19763, 19777, 19793,
+ 19801, 19813, 19819, 19841, 19843, 19853, 19861, 19867, 19889, 19891,
+ 19913, 19919, 19927, 19937, 19949, 19961, 19963, 19973, 19979, 19991,
+ 19993, 19997, 20011, 20021, 20023, 20029, 20047, 20051, 20063, 20071,
+ 20089, 20101, 20107, 20113, 20117, 20123, 20129, 20143, 20147, 20149,
+ 20161, 20173, 20177, 20183, 20201, 20219, 20231, 20233, 20249, 20261,
+ 20269, 20287, 20297, 20323, 20327, 20333, 20341, 20347, 20353, 20357,
+ 20359, 20369, 20389, 20393, 20399, 20407, 20411, 20431, 20441, 20443,
+ 20477, 20479, 20483, 20507, 20509, 20521, 20533, 20543, 20549, 20551,
+ 20563, 20593, 20599, 20611, 20627, 20639, 20641, 20663, 20681, 20693,
+ 20707, 20717, 20719, 20731, 20743, 20747, 20749, 20753, 20759, 20771,
+ 20773, 20789, 20807, 20809, 20849, 20857, 20873, 20879, 20887, 20897,
+ 20899, 20903, 20921, 20929, 20939, 20947, 20959, 20963, 20981, 20983,
+ 21001, 21011, 21013, 21017, 21019, 21023, 21031, 21059, 21061, 21067,
+ 21089, 21101, 21107, 21121, 21139, 21143, 21149, 21157, 21163, 21169,
+ 21179, 21187, 21191, 21193, 21211, 21221, 21227, 21247, 21269, 21277,
+ 21283, 21313, 21317, 21319, 21323, 21341, 21347, 21377, 21379, 21383,
+ 21391, 21397, 21401, 21407, 21419, 21433, 21467, 21481, 21487, 21491,
+ 21493, 21499, 21503, 21517, 21521, 21523, 21529, 21557, 21559, 21563,
+ 21569, 21577, 21587, 21589, 21599, 21601, 21611, 21613, 21617, 21647,
+ 21649, 21661, 21673, 21683, 21701, 21713, 21727, 21737, 21739, 21751,
+ 21757, 21767, 21773, 21787, 21799, 21803, 21817, 21821, 21839, 21841,
+ 21851, 21859, 21863, 21871, 21881, 21893, 21911, 21929, 21937, 21943,
+ 21961, 21977, 21991, 21997, 22003, 22013, 22027, 22031, 22037, 22039,
+ 22051, 22063, 22067, 22073, 22079, 22091, 22093, 22109, 22111, 22123,
+ 22129, 22133, 22147, 22153, 22157, 22159, 22171, 22189, 22193, 22229,
+ 22247, 22259, 22271, 22273, 22277, 22279, 22283, 22291, 22303, 22307,
+ 22343, 22349, 22367, 22369, 22381, 22391, 22397, 22409, 22433, 22441,
+ 22447, 22453, 22469, 22481, 22483, 22501, 22511, 22531, 22541, 22543,
+ 22549, 22567, 22571, 22573, 22613, 22619, 22621, 22637, 22639, 22643,
+ 22651, 22669, 22679, 22691, 22697, 22699, 22709, 22717, 22721, 22727,
+ 22739, 22741, 22751, 22769, 22777, 22783, 22787, 22807, 22811, 22817,
+ 22853, 22859, 22861, 22871, 22877, 22901, 22907, 22921, 22937, 22943,
+ 22961, 22963, 22973, 22993, 23003, 23011, 23017, 23021, 23027, 23029,
+ 23039, 23041, 23053, 23057, 23059, 23063, 23071, 23081, 23087, 23099,
+ 23117, 23131, 23143, 23159, 23167, 23173, 23189, 23197, 23201, 23203,
+ 23209, 23227, 23251, 23269, 23279, 23291, 23293, 23297, 23311, 23321,
+ 23327, 23333, 23339, 23357, 23369, 23371, 23399, 23417, 23431, 23447,
+ 23459, 23473, 23497, 23509, 23531, 23537, 23539, 23549, 23557, 23561,
+ 23563, 23567, 23581, 23593, 23599, 23603, 23609, 23623, 23627, 23629,
+ 23633, 23663, 23669, 23671, 23677, 23687, 23689, 23719, 23741, 23743,
+ 23747, 23753, 23761, 23767, 23773, 23789, 23801, 23813, 23819, 23827,
+ 23831, 23833, 23857, 23869, 23873, 23879, 23887, 23893, 23899, 23909,
+ 23911, 23917, 23929, 23957, 23971, 23977, 23981, 23993, 24001, 24007,
+ 24019, 24023, 24029, 24043, 24049, 24061, 24071, 24077, 24083, 24091,
+ 24097, 24103, 24107, 24109, 24113, 24121, 24133, 24137, 24151, 24169,
+ 24179, 24181, 24197, 24203, 24223, 24229, 24239, 24247, 24251, 24281,
+ 24317, 24329, 24337, 24359, 24371, 24373, 24379, 24391, 24407, 24413,
+ 24419, 24421, 24439, 24443, 24469, 24473, 24481, 24499, 24509, 24517,
+ 24527, 24533, 24547, 24551, 24571, 24593, 24611, 24623, 24631, 24659,
+ 24671, 24677, 24683, 24691, 24697, 24709, 24733, 24749, 24763, 24767,
+ 24781, 24793, 24799, 24809, 24821, 24841, 24847, 24851, 24859, 24877,
+ 24889, 24907, 24917, 24919, 24923, 24943, 24953, 24967, 24971, 24977,
+ 24979, 24989, 25013, 25031, 25033, 25037, 25057, 25073, 25087, 25097,
+ 25111, 25117, 25121, 25127, 25147, 25153, 25163, 25169, 25171, 25183,
+ 25189, 25219, 25229, 25237, 25243, 25247, 25253, 25261, 25301, 25303,
+ 25307, 25309, 25321, 25339, 25343, 25349, 25357, 25367, 25373, 25391,
+ 25409, 25411, 25423, 25439, 25447, 25453, 25457, 25463, 25469, 25471,
+ 25523, 25537, 25541, 25561, 25577, 25579, 25583, 25589, 25601, 25603,
+ 25609, 25621, 25633, 25639, 25643, 25657, 25667, 25673, 25679, 25693,
+ 25703, 25717, 25733, 25741, 25747, 25759, 25763, 25771, 25793, 25799,
+ 25801, 25819, 25841, 25847, 25849, 25867, 25873, 25889, 25903, 25913,
+ 25919, 25931, 25933, 25939, 25943, 25951, 25969, 25981, 25997, 25999,
+ 26003, 26017, 26021, 26029, 26041, 26053, 26083, 26099, 26107, 26111,
+ 26113, 26119, 26141, 26153, 26161, 26171, 26177, 26183, 26189, 26203,
+ 26209, 26227, 26237, 26249, 26251, 26261, 26263, 26267, 26293, 26297,
+ 26309, 26317, 26321, 26339, 26347, 26357, 26371, 26387, 26393, 26399,
+ 26407, 26417, 26423, 26431, 26437, 26449, 26459, 26479, 26489, 26497,
+ 26501, 26513, 26539, 26557, 26561, 26573, 26591, 26597, 26627, 26633,
+ 26641, 26647, 26669, 26681, 26683, 26687, 26693, 26699, 26701, 26711,
+ 26713, 26717, 26723, 26729, 26731, 26737, 26759, 26777, 26783, 26801,
+ 26813, 26821, 26833, 26839, 26849, 26861, 26863, 26879, 26881, 26891,
+ 26893, 26903, 26921, 26927, 26947, 26951, 26953, 26959, 26981, 26987,
+ 26993, 27011, 27017, 27031, 27043, 27059, 27061, 27067, 27073, 27077,
+ 27091, 27103, 27107, 27109, 27127, 27143, 27179, 27191, 27197, 27211,
+ 27239, 27241, 27253, 27259, 27271, 27277, 27281, 27283, 27299, 27329,
+ 27337, 27361, 27367, 27397, 27407, 27409, 27427, 27431, 27437, 27449,
+ 27457, 27479, 27481, 27487, 27509, 27527, 27529, 27539, 27541, 27551,
+ 27581, 27583, 27611, 27617, 27631, 27647, 27653, 27673, 27689, 27691,
+ 27697, 27701, 27733, 27737, 27739, 27743, 27749, 27751, 27763, 27767,
+ 27773, 27779, 27791, 27793, 27799, 27803, 27809, 27817, 27823, 27827,
+ 27847, 27851, 27883, 27893, 27901, 27917, 27919, 27941, 27943, 27947,
+ 27953, 27961, 27967, 27983, 27997, 28001, 28019, 28027, 28031, 28051,
+ 28057, 28069, 28081, 28087, 28097, 28099, 28109, 28111, 28123, 28151,
+ 28163, 28181, 28183, 28201, 28211, 28219, 28229, 28277, 28279, 28283,
+ 28289, 28297, 28307, 28309, 28319, 28349, 28351, 28387, 28393, 28403,
+ 28409, 28411, 28429, 28433, 28439, 28447, 28463, 28477, 28493, 28499,
+ 28513, 28517, 28537, 28541, 28547, 28549, 28559, 28571, 28573, 28579,
+ 28591, 28597, 28603, 28607, 28619, 28621, 28627, 28631, 28643, 28649,
+ 28657, 28661, 28663, 28669, 28687, 28697, 28703, 28711, 28723, 28729,
+ 28751, 28753, 28759, 28771, 28789, 28793, 28807, 28813, 28817, 28837,
+ 28843, 28859, 28867, 28871, 28879, 28901, 28909, 28921, 28927, 28933,
+ 28949, 28961, 28979, 29009, 29017, 29021, 29023, 29027, 29033, 29059,
+ 29063, 29077, 29101, 29123, 29129, 29131, 29137, 29147, 29153, 29167,
+ 29173, 29179, 29191, 29201, 29207, 29209, 29221, 29231, 29243, 29251,
+ 29269, 29287, 29297, 29303, 29311, 29327, 29333, 29339, 29347, 29363,
+ 29383, 29387, 29389, 29399, 29401, 29411, 29423, 29429, 29437, 29443,
+ 29453, 29473, 29483, 29501, 29527, 29531, 29537, 29567, 29569, 29573,
+ 29581, 29587, 29599, 29611, 29629, 29633, 29641, 29663, 29669, 29671,
+ 29683, 29717, 29723, 29741, 29753, 29759, 29761, 29789, 29803, 29819,
+ 29833, 29837, 29851, 29863, 29867, 29873, 29879, 29881, 29917, 29921,
+ 29927, 29947, 29959, 29983, 29989, 30011, 30013, 30029, 30047, 30059,
+ 30071, 30089, 30091, 30097, 30103, 30109, 30113, 30119, 30133, 30137,
+ 30139, 30161, 30169, 30181, 30187, 30197, 30203, 30211, 30223, 30241,
+ 30253, 30259, 30269, 30271, 30293, 30307, 30313, 30319, 30323, 30341,
+ 30347, 30367, 30389, 30391, 30403, 30427, 30431, 30449, 30467, 30469,
+ 30491, 30493, 30497, 30509, 30517, 30529, 30539, 30553, 30557, 30559,
+ 30577, 30593, 30631, 30637, 30643, 30649, 30661, 30671, 30677, 30689,
+ 30697, 30703, 30707, 30713, 30727, 30757, 30763, 30773, 30781, 30803,
+ 30809, 30817, 30829, 30839, 30841, 30851, 30853, 30859, 30869, 30871,
+ 30881, 30893, 30911, 30931, 30937, 30941, 30949, 30971, 30977, 30983,
+ 31013, 31019, 31033, 31039, 31051, 31063, 31069, 31079, 31081, 31091,
+ 31121, 31123, 31139, 31147, 31151, 31153, 31159, 31177, 31181, 31183,
+ 31189, 31193, 31219, 31223, 31231, 31237, 31247, 31249, 31253, 31259,
+ 31267, 31271, 31277, 31307, 31319, 31321, 31327, 31333, 31337, 31357,
+ 31379, 31387, 31391, 31393, 31397, 31469, 31477, 31481, 31489, 31511,
+ 31513, 31517, 31531, 31541, 31543, 31547, 31567, 31573, 31583, 31601,
+ 31607, 31627, 31643, 31649, 31657, 31663, 31667, 31687, 31699, 31721,
+ 31723, 31727, 31729, 31741, 31751, 31769, 31771, 31793, 31799, 31817,
+ 31847, 31849, 31859, 31873, 31883, 31891, 31907, 31957, 31963, 31973,
+ 31981, 31991, 32003, 32009, 32027, 32029, 32051, 32057, 32059, 32063,
+ 32069, 32077, 32083, 32089, 32099, 32117, 32119, 32141, 32143, 32159,
+ 32173, 32183, 32189, 32191, 32203, 32213, 32233, 32237, 32251, 32257,
+ 32261, 32297, 32299, 32303, 32309, 32321, 32323, 32327, 32341, 32353,
+ 32359, 32363, 32369, 32371, 32377, 32381, 32401, 32411, 32413, 32423,
+ 32429, 32441, 32443, 32467, 32479, 32491, 32497, 32503, 32507, 32531,
+ 32533, 32537, 32561, 32563, 32569, 32573, 32579, 32587, 32603, 32609,
+ 32611, 32621, 32633, 32647, 32653, 32687, 32693, 32707, 32713, 32717,
+ 32719, 32749, 32771, 32779, 32783, 32789, 32797, 32801, 32803, 32831,
+ 32833, 32839, 32843, 32869, 32887, 32909, 32911, 32917, 32933, 32939,
+ 32941, 32957, 32969, 32971, 32983, 32987, 32993, 32999, 33013, 33023,
+ 33029, 33037, 33049, 33053, 33071, 33073, 33083, 33091, 33107, 33113,
+ 33119, 33149, 33151, 33161, 33179, 33181, 33191, 33199, 33203, 33211,
+ 33223, 33247, 33287, 33289, 33301, 33311, 33317, 33329, 33331, 33343,
+ 33347, 33349, 33353, 33359, 33377, 33391, 33403, 33409, 33413, 33427,
+ 33457, 33461, 33469, 33479, 33487, 33493, 33503, 33521, 33529, 33533,
+ 33547, 33563, 33569, 33577, 33581, 33587, 33589, 33599, 33601, 33613,
+ 33617, 33619, 33623, 33629, 33637, 33641, 33647, 33679, 33703, 33713,
+ 33721, 33739, 33749, 33751, 33757, 33767, 33769, 33773, 33791, 33797,
+ 33809, 33811, 33827, 33829, 33851, 33857, 33863, 33871, 33889, 33893,
+ 33911, 33923, 33931, 33937, 33941, 33961, 33967, 33997, 34019, 34031,
+ 34033, 34039, 34057, 34061, 34123, 34127, 34129, 34141, 34147, 34157,
+ 34159, 34171, 34183, 34211, 34213, 34217, 34231, 34253, 34259, 34261,
+ 34267, 34273, 34283, 34297, 34301, 34303, 34313, 34319, 34327, 34337,
+ 34351, 34361, 34367, 34369, 34381, 34403, 34421, 34429, 34439, 34457,
+ 34469, 34471, 34483, 34487, 34499, 34501, 34511, 34513, 34519, 34537,
+ 34543, 34549, 34583, 34589, 34591, 34603, 34607, 34613, 34631, 34649,
+ 34651, 34667, 34673, 34679, 34687, 34693, 34703, 34721, 34729, 34739,
+ 34747, 34757, 34759, 34763, 34781, 34807, 34819, 34841, 34843, 34847,
+ 34849, 34871, 34877, 34883, 34897, 34913, 34919, 34939, 34949, 34961,
+ 34963, 34981, 35023, 35027, 35051, 35053, 35059, 35069, 35081, 35083,
+ 35089, 35099, 35107, 35111, 35117, 35129, 35141, 35149, 35153, 35159,
+ 35171, 35201, 35221, 35227, 35251, 35257, 35267, 35279, 35281, 35291,
+ 35311, 35317, 35323, 35327, 35339, 35353, 35363, 35381, 35393, 35401,
+ 35407, 35419, 35423, 35437, 35447, 35449, 35461, 35491, 35507, 35509,
+ 35521, 35527, 35531, 35533, 35537, 35543, 35569, 35573, 35591, 35593,
+ 35597, 35603, 35617, 35671, 35677, 35729, 35731, 35747, 35753, 35759,
+ 35771, 35797, 35801, 35803, 35809, 35831, 35837, 35839, 35851, 35863,
+ 35869, 35879, 35897, 35899, 35911, 35923, 35933, 35951, 35963, 35969,
+ 35977, 35983, 35993, 35999, 36007, 36011, 36013, 36017, 36037, 36061,
+ 36067, 36073, 36083, 36097, 36107, 36109, 36131, 36137, 36151, 36161,
+ 36187, 36191, 36209, 36217, 36229, 36241, 36251, 36263, 36269, 36277,
+ 36293, 36299, 36307, 36313, 36319, 36341, 36343, 36353, 36373, 36383,
+ 36389, 36433, 36451, 36457, 36467, 36469, 36473, 36479, 36493, 36497,
+ 36523, 36527, 36529, 36541, 36551, 36559, 36563, 36571, 36583, 36587,
+ 36599, 36607, 36629, 36637, 36643, 36653, 36671, 36677, 36683, 36691,
+ 36697, 36709, 36713, 36721, 36739, 36749, 36761, 36767, 36779, 36781,
+ 36787, 36791, 36793, 36809, 36821, 36833, 36847, 36857, 36871, 36877,
+ 36887, 36899, 36901, 36913, 36919, 36923, 36929, 36931, 36943, 36947,
+ 36973, 36979, 36997, 37003, 37013, 37019, 37021, 37039, 37049, 37057,
+ 37061, 37087, 37097, 37117, 37123, 37139, 37159, 37171, 37181, 37189,
+ 37199, 37201, 37217, 37223, 37243, 37253, 37273, 37277, 37307, 37309,
+ 37313, 37321, 37337, 37339, 37357, 37361, 37363, 37369, 37379, 37397,
+ 37409, 37423, 37441, 37447, 37463, 37483, 37489, 37493, 37501, 37507,
+ 37511, 37517, 37529, 37537, 37547, 37549, 37561, 37567, 37571, 37573,
+ 37579, 37589, 37591, 37607, 37619, 37633, 37643, 37649, 37657, 37663,
+ 37691, 37693, 37699, 37717, 37747, 37781, 37783, 37799, 37811, 37813,
+ 37831, 37847, 37853, 37861, 37871, 37879, 37889, 37897, 37907, 37951,
+ 37957, 37963, 37967, 37987, 37991, 37993, 37997, 38011, 38039, 38047,
+ 38053, 38069, 38083, 38113, 38119, 38149, 38153, 38167, 38177, 38183,
+ 38189, 38197, 38201, 38219, 38231, 38237, 38239, 38261, 38273, 38281,
+ 38287, 38299, 38303, 38317, 38321, 38327, 38329, 38333, 38351, 38371,
+ 38377, 38393, 38431, 38447, 38449, 38453, 38459, 38461, 38501, 38543,
+ 38557, 38561, 38567, 38569, 38593, 38603, 38609, 38611, 38629, 38639,
+ 38651, 38653, 38669, 38671, 38677, 38693, 38699, 38707, 38711, 38713,
+ 38723, 38729, 38737, 38747, 38749, 38767, 38783, 38791, 38803, 38821,
+ 38833, 38839, 38851, 38861, 38867, 38873, 38891, 38903, 38917, 38921,
+ 38923, 38933, 38953, 38959, 38971, 38977, 38993, 39019, 39023, 39041,
+ 39043, 39047, 39079, 39089, 39097, 39103, 39107, 39113, 39119, 39133,
+ 39139, 39157, 39161, 39163, 39181, 39191, 39199, 39209, 39217, 39227,
+ 39229, 39233, 39239, 39241, 39251, 39293, 39301, 39313, 39317, 39323,
+ 39341, 39343, 39359, 39367, 39371, 39373, 39383, 39397, 39409, 39419,
+ 39439, 39443, 39451, 39461, 39499, 39503, 39509, 39511, 39521, 39541,
+ 39551, 39563, 39569, 39581, 39607, 39619, 39623, 39631, 39659, 39667,
+ 39671, 39679, 39703, 39709, 39719, 39727, 39733, 39749, 39761, 39769,
+ 39779, 39791, 39799, 39821, 39827, 39829, 39839, 39841, 39847, 39857,
+ 39863, 39869, 39877, 39883, 39887, 39901, 39929, 39937, 39953, 39971,
+ 39979, 39983, 39989, 40009, 40013, 40031, 40037, 40039, 40063, 40087,
+ 40093, 40099, 40111, 40123, 40127, 40129, 40151, 40153, 40163, 40169,
+ 40177, 40189, 40193, 40213, 40231, 40237, 40241, 40253, 40277, 40283,
+ 40289, 40343, 40351, 40357, 40361, 40387, 40423, 40427, 40429, 40433,
+ 40459, 40471, 40483, 40487, 40493, 40499, 40507, 40519, 40529, 40531,
+ 40543, 40559, 40577, 40583, 40591, 40597, 40609, 40627, 40637, 40639,
+ 40693, 40697, 40699, 40709, 40739, 40751, 40759, 40763, 40771, 40787,
+ 40801, 40813, 40819, 40823, 40829, 40841, 40847, 40849, 40853, 40867,
+ 40879, 40883, 40897, 40903, 40927, 40933, 40939, 40949, 40961, 40973,
+ 40993, 41011, 41017, 41023, 41039, 41047, 41051, 41057, 41077, 41081,
+ 41113, 41117, 41131, 41141, 41143, 41149, 41161, 41177, 41179, 41183,
+ 41189, 41201, 41203, 41213, 41221, 41227, 41231, 41233, 41243, 41257,
+ 41263, 41269, 41281, 41299, 41333, 41341, 41351, 41357, 41381, 41387,
+ 41389, 41399, 41411, 41413, 41443, 41453, 41467, 41479, 41491, 41507,
+ 41513, 41519, 41521, 41539, 41543, 41549, 41579, 41593, 41597, 41603,
+ 41609, 41611, 41617, 41621, 41627, 41641, 41647, 41651, 41659, 41669,
+ 41681, 41687, 41719, 41729, 41737, 41759, 41761, 41771, 41777, 41801,
+ 41809, 41813, 41843, 41849, 41851, 41863, 41879, 41887, 41893, 41897,
+ 41903, 41911, 41927, 41941, 41947, 41953, 41957, 41959, 41969, 41981,
+ 41983, 41999, 42013, 42017, 42019, 42023, 42043, 42061, 42071, 42073,
+ 42083, 42089, 42101, 42131, 42139, 42157, 42169, 42179, 42181, 42187,
+ 42193, 42197, 42209, 42221, 42223, 42227, 42239, 42257, 42281, 42283,
+ 42293, 42299, 42307, 42323, 42331, 42337, 42349, 42359, 42373, 42379,
+ 42391, 42397, 42403, 42407, 42409, 42433, 42437, 42443, 42451, 42457,
+ 42461, 42463, 42467, 42473, 42487, 42491, 42499, 42509, 42533, 42557,
+ 42569, 42571, 42577, 42589, 42611, 42641, 42643, 42649, 42667, 42677,
+ 42683, 42689, 42697, 42701, 42703, 42709, 42719, 42727, 42737, 42743,
+ 42751, 42767, 42773, 42787, 42793, 42797, 42821, 42829, 42839, 42841,
+ 42853, 42859, 42863, 42899, 42901, 42923, 42929, 42937, 42943, 42953,
+ 42961, 42967, 42979, 42989, 43003, 43013, 43019, 43037, 43049, 43051,
+ 43063, 43067, 43093, 43103, 43117, 43133, 43151, 43159, 43177, 43189,
+ 43201, 43207, 43223, 43237, 43261, 43271, 43283, 43291, 43313, 43319,
+ 43321, 43331, 43391, 43397, 43399, 43403, 43411, 43427, 43441, 43451,
+ 43457, 43481, 43487, 43499, 43517, 43541, 43543, 43573, 43577, 43579,
+ 43591, 43597, 43607, 43609, 43613, 43627, 43633, 43649, 43651, 43661,
+ 43669, 43691, 43711, 43717, 43721, 43753, 43759, 43777, 43781, 43783,
+ 43787, 43789, 43793, 43801, 43853, 43867, 43889, 43891, 43913, 43933,
+ 43943, 43951, 43961, 43963, 43969, 43973, 43987, 43991, 43997, 44017,
+ 44021, 44027, 44029, 44041, 44053, 44059, 44071, 44087, 44089, 44101,
+ 44111, 44119, 44123, 44129, 44131, 44159, 44171, 44179, 44189, 44201,
+ 44203, 44207, 44221, 44249, 44257, 44263, 44267, 44269, 44273, 44279,
+ 44281, 44293, 44351, 44357, 44371, 44381, 44383, 44389, 44417, 44449,
+ 44453, 44483, 44491, 44497, 44501, 44507, 44519, 44531, 44533, 44537,
+ 44543, 44549, 44563, 44579, 44587, 44617, 44621, 44623, 44633, 44641,
+ 44647, 44651, 44657, 44683, 44687, 44699, 44701, 44711, 44729, 44741,
+ 44753, 44771, 44773, 44777, 44789, 44797, 44809, 44819, 44839, 44843,
+ 44851, 44867, 44879, 44887, 44893, 44909, 44917, 44927, 44939, 44953,
+ 44959, 44963, 44971, 44983, 44987, 45007, 45013, 45053, 45061, 45077,
+ 45083, 45119, 45121, 45127, 45131, 45137, 45139, 45161, 45179, 45181,
+ 45191, 45197, 45233, 45247, 45259, 45263, 45281, 45289, 45293, 45307,
+ 45317, 45319, 45329, 45337, 45341, 45343, 45361, 45377, 45389, 45403,
+ 45413, 45427, 45433, 45439, 45481, 45491, 45497, 45503, 45523, 45533,
+ 45541, 45553, 45557, 45569, 45587, 45589, 45599, 45613, 45631, 45641,
+ 45659, 45667, 45673, 45677, 45691, 45697, 45707, 45737, 45751, 45757,
+ 45763, 45767, 45779, 45817, 45821, 45823, 45827, 45833, 45841, 45853,
+ 45863, 45869, 45887, 45893, 45943, 45949, 45953, 45959, 45971, 45979,
+ 45989, 46021, 46027, 46049, 46051, 46061, 46073, 46091, 46093, 46099,
+ 46103, 46133, 46141, 46147, 46153, 46171, 46181, 46183, 46187, 46199,
+ 46219, 46229, 46237, 46261, 46271, 46273, 46279, 46301, 46307, 46309,
+ 46327, 46337, 46349, 46351, 46381, 46399, 46411, 46439, 46441, 46447,
+ 46451, 46457, 46471, 46477, 46489, 46499, 46507, 46511, 46523, 46549,
+ 46559, 46567, 46573, 46589, 46591, 46601, 46619, 46633, 46639, 46643,
+ 46649, 46663, 46679, 46681, 46687, 46691, 46703, 46723, 46727, 46747,
+ 46751, 46757, 46769, 46771, 46807, 46811, 46817, 46819, 46829, 46831,
+ 46853, 46861, 46867, 46877, 46889, 46901, 46919, 46933, 46957, 46993,
+ 46997, 47017, 47041, 47051, 47057, 47059, 47087, 47093, 47111, 47119,
+ 47123, 47129, 47137, 47143, 47147, 47149, 47161, 47189, 47207, 47221,
+ 47237, 47251, 47269, 47279, 47287, 47293, 47297, 47303, 47309, 47317,
+ 47339, 47351, 47353, 47363, 47381, 47387, 47389, 47407, 47417, 47419,
+ 47431, 47441, 47459, 47491, 47497, 47501, 47507, 47513, 47521, 47527,
+ 47533, 47543, 47563, 47569, 47581, 47591, 47599, 47609, 47623, 47629,
+ 47639, 47653, 47657, 47659, 47681, 47699, 47701, 47711, 47713, 47717,
+ 47737, 47741, 47743, 47777, 47779, 47791, 47797, 47807, 47809, 47819,
+ 47837, 47843, 47857, 47869, 47881, 47903, 47911, 47917, 47933, 47939,
+ 47947, 47951, 47963, 47969, 47977, 47981, 48017, 48023, 48029, 48049,
+ 48073, 48079, 48091, 48109, 48119, 48121, 48131, 48157, 48163, 48179,
+ 48187, 48193, 48197, 48221, 48239, 48247, 48259, 48271, 48281, 48299,
+ 48311, 48313, 48337, 48341, 48353, 48371, 48383, 48397, 48407, 48409,
+ 48413, 48437, 48449, 48463, 48473, 48479, 48481, 48487, 48491, 48497,
+ 48523, 48527, 48533, 48539, 48541, 48563, 48571, 48589, 48593, 48611,
+ 48619, 48623, 48647, 48649, 48661, 48673, 48677, 48679, 48731, 48733,
+ 48751, 48757, 48761, 48767, 48779, 48781, 48787, 48799, 48809, 48817,
+ 48821, 48823, 48847, 48857, 48859, 48869, 48871, 48883, 48889, 48907,
+ 48947, 48953, 48973, 48989, 48991, 49003, 49009, 49019, 49031, 49033,
+ 49037, 49043, 49057, 49069, 49081, 49103, 49109, 49117, 49121, 49123,
+ 49139, 49157, 49169, 49171, 49177, 49193, 49199, 49201, 49207, 49211,
+ 49223, 49253, 49261, 49277, 49279, 49297, 49307, 49331, 49333, 49339,
+ 49363, 49367, 49369, 49391, 49393, 49409, 49411, 49417, 49429, 49433,
+ 49451, 49459, 49463, 49477, 49481, 49499, 49523, 49529, 49531, 49537,
+ 49547, 49549, 49559, 49597, 49603, 49613, 49627, 49633, 49639, 49663,
+ 49667, 49669, 49681, 49697, 49711, 49727, 49739, 49741, 49747, 49757,
+ 49783, 49787, 49789, 49801, 49807, 49811, 49823, 49831, 49843, 49853,
+ 49871, 49877, 49891, 49919, 49921, 49927, 49937, 49939, 49943, 49957,
+ 49991, 49993, 49999, 50021, 50023, 50033, 50047, 50051, 50053, 50069,
+ 50077, 50087, 50093, 50101, 50111, 50119, 50123, 50129, 50131, 50147,
+ 50153, 50159, 50177, 50207, 50221, 50227, 50231, 50261, 50263, 50273,
+ 50287, 50291, 50311, 50321, 50329, 50333, 50341, 50359, 50363, 50377,
+ 50383, 50387, 50411, 50417, 50423, 50441, 50459, 50461, 50497, 50503,
+ 50513, 50527, 50539, 50543, 50549, 50551, 50581, 50587, 50591, 50593,
+ 50599, 50627, 50647, 50651, 50671, 50683, 50707, 50723, 50741, 50753,
+ 50767, 50773, 50777, 50789, 50821, 50833, 50839, 50849, 50857, 50867,
+ 50873, 50891, 50893, 50909, 50923, 50929, 50951, 50957, 50969, 50971,
+ 50989, 50993, 51001, 51031, 51043, 51047, 51059, 51061, 51071, 51109,
+ 51131, 51133, 51137, 51151, 51157, 51169, 51193, 51197, 51199, 51203,
+ 51217, 51229, 51239, 51241, 51257, 51263, 51283, 51287, 51307, 51329,
+ 51341, 51343, 51347, 51349, 51361, 51383, 51407, 51413, 51419, 51421,
+ 51427, 51431, 51437, 51439, 51449, 51461, 51473, 51479, 51481, 51487,
+ 51503, 51511, 51517, 51521, 51539, 51551, 51563, 51577, 51581, 51593,
+ 51599, 51607, 51613, 51631, 51637, 51647, 51659, 51673, 51679, 51683,
+ 51691, 51713, 51719, 51721, 51749, 51767, 51769, 51787, 51797, 51803,
+ 51817, 51827, 51829, 51839, 51853, 51859, 51869, 51871, 51893, 51899,
+ 51907, 51913, 51929, 51941, 51949, 51971, 51973, 51977, 51991, 52009,
+ 52021, 52027, 52051, 52057, 52067, 52069, 52081, 52103, 52121, 52127,
+ 52147, 52153, 52163, 52177, 52181, 52183, 52189, 52201, 52223, 52237,
+ 52249, 52253, 52259, 52267, 52289, 52291, 52301, 52313, 52321, 52361,
+ 52363, 52369, 52379, 52387, 52391, 52433, 52453, 52457, 52489, 52501,
+ 52511, 52517, 52529, 52541, 52543, 52553, 52561, 52567, 52571, 52579,
+ 52583, 52609, 52627, 52631, 52639, 52667, 52673, 52691, 52697, 52709,
+ 52711, 52721, 52727, 52733, 52747, 52757, 52769, 52783, 52807, 52813,
+ 52817, 52837, 52859, 52861, 52879, 52883, 52889, 52901, 52903, 52919,
+ 52937, 52951, 52957, 52963, 52967, 52973, 52981, 52999, 53003, 53017,
+ 53047, 53051, 53069, 53077, 53087, 53089, 53093, 53101, 53113, 53117,
+ 53129, 53147, 53149, 53161, 53171, 53173, 53189, 53197, 53201, 53231,
+ 53233, 53239, 53267, 53269, 53279, 53281, 53299, 53309, 53323, 53327,
+ 53353, 53359, 53377, 53381, 53401, 53407, 53411, 53419, 53437, 53441,
+ 53453, 53479, 53503, 53507, 53527, 53549, 53551, 53569, 53591, 53593,
+ 53597, 53609, 53611, 53617, 53623, 53629, 53633, 53639, 53653, 53657,
+ 53681, 53693, 53699, 53717, 53719, 53731, 53759, 53773, 53777, 53783,
+ 53791, 53813, 53819, 53831, 53849, 53857, 53861, 53881, 53887, 53891,
+ 53897, 53899, 53917, 53923, 53927, 53939, 53951, 53959, 53987, 53993,
+ 54001, 54011, 54013, 54037, 54049, 54059, 54083, 54091, 54101, 54121,
+ 54133, 54139, 54151, 54163, 54167, 54181, 54193, 54217, 54251, 54269,
+ 54277, 54287, 54293, 54311, 54319, 54323, 54331, 54347, 54361, 54367,
+ 54371, 54377, 54401, 54403, 54409, 54413, 54419, 54421, 54437, 54443,
+ 54449, 54469, 54493, 54497, 54499, 54503, 54517, 54521, 54539, 54541,
+ 54547, 54559, 54563, 54577, 54581, 54583, 54601, 54617, 54623, 54629,
+ 54631, 54647, 54667, 54673, 54679, 54709, 54713, 54721, 54727, 54751,
+ 54767, 54773, 54779, 54787, 54799, 54829, 54833, 54851, 54869, 54877,
+ 54881, 54907, 54917, 54919, 54941, 54949, 54959, 54973, 54979, 54983,
+ 55001, 55009, 55021, 55049, 55051, 55057, 55061, 55073, 55079, 55103,
+ 55109, 55117, 55127, 55147, 55163, 55171, 55201, 55207, 55213, 55217,
+ 55219, 55229, 55243, 55249, 55259, 55291, 55313, 55331, 55333, 55337,
+ 55339, 55343, 55351, 55373, 55381, 55399, 55411, 55439, 55441, 55457,
+ 55469, 55487, 55501, 55511, 55529, 55541, 55547, 55579, 55589, 55603,
+ 55609, 55619, 55621, 55631, 55633, 55639, 55661, 55663, 55667, 55673,
+ 55681, 55691, 55697, 55711, 55717, 55721, 55733, 55763, 55787, 55793,
+ 55799, 55807, 55813, 55817, 55819, 55823, 55829, 55837, 55843, 55849,
+ 55871, 55889, 55897, 55901, 55903, 55921, 55927, 55931, 55933, 55949,
+ 55967, 55987, 55997, 56003, 56009, 56039, 56041, 56053, 56081, 56087,
+ 56093, 56099, 56101, 56113, 56123, 56131, 56149, 56167, 56171, 56179,
+ 56197, 56207, 56209, 56237, 56239, 56249, 56263, 56267, 56269, 56299,
+ 56311, 56333, 56359, 56369, 56377, 56383, 56393, 56401, 56417, 56431,
+ 56437, 56443, 56453, 56467, 56473, 56477, 56479, 56489, 56501, 56503,
+ 56509, 56519, 56527, 56531, 56533, 56543, 56569, 56591, 56597, 56599,
+ 56611, 56629, 56633, 56659, 56663, 56671, 56681, 56687, 56701, 56711,
+ 56713, 56731, 56737, 56747, 56767, 56773, 56779, 56783, 56807, 56809,
+ 56813, 56821, 56827, 56843, 56857, 56873, 56891, 56893, 56897, 56909,
+ 56911, 56921, 56923, 56929, 56941, 56951, 56957, 56963, 56983, 56989,
+ 56993, 56999, 57037, 57041, 57047, 57059, 57073, 57077, 57089, 57097,
+ 57107, 57119, 57131, 57139, 57143, 57149, 57163, 57173, 57179, 57191,
+ 57193, 57203, 57221, 57223, 57241, 57251, 57259, 57269, 57271, 57283,
+ 57287, 57301, 57329, 57331, 57347, 57349, 57367, 57373, 57383, 57389,
+ 57397, 57413, 57427, 57457, 57467, 57487, 57493, 57503, 57527, 57529,
+ 57557, 57559, 57571, 57587, 57593, 57601, 57637, 57641, 57649, 57653,
+ 57667, 57679, 57689, 57697, 57709, 57713, 57719, 57727, 57731, 57737,
+ 57751, 57773, 57781, 57787, 57791, 57793, 57803, 57809, 57829, 57839,
+ 57847, 57853, 57859, 57881, 57899, 57901, 57917, 57923, 57943, 57947,
+ 57973, 57977, 57991, 58013, 58027, 58031, 58043, 58049, 58057, 58061,
+ 58067, 58073, 58099, 58109, 58111, 58129, 58147, 58151, 58153, 58169,
+ 58171, 58189, 58193, 58199, 58207, 58211, 58217, 58229, 58231, 58237,
+ 58243, 58271, 58309, 58313, 58321, 58337, 58363, 58367, 58369, 58379,
+ 58391, 58393, 58403, 58411, 58417, 58427, 58439, 58441, 58451, 58453,
+ 58477, 58481, 58511, 58537, 58543, 58549, 58567, 58573, 58579, 58601,
+ 58603, 58613, 58631, 58657, 58661, 58679, 58687, 58693, 58699, 58711,
+ 58727, 58733, 58741, 58757, 58763, 58771, 58787, 58789, 58831, 58889,
+ 58897, 58901, 58907, 58909, 58913, 58921, 58937, 58943, 58963, 58967,
+ 58979, 58991, 58997, 59009, 59011, 59021, 59023, 59029, 59051, 59053,
+ 59063, 59069, 59077, 59083, 59093, 59107, 59113, 59119, 59123, 59141,
+ 59149, 59159, 59167, 59183, 59197, 59207, 59209, 59219, 59221, 59233,
+ 59239, 59243, 59263, 59273, 59281, 59333, 59341, 59351, 59357, 59359,
+ 59369, 59377, 59387, 59393, 59399, 59407, 59417, 59419, 59441, 59443,
+ 59447, 59453, 59467, 59471, 59473, 59497, 59509, 59513, 59539, 59557,
+ 59561, 59567, 59581, 59611, 59617, 59621, 59627, 59629, 59651, 59659,
+ 59663, 59669, 59671, 59693, 59699, 59707, 59723, 59729, 59743, 59747,
+ 59753, 59771, 59779, 59791, 59797, 59809, 59833, 59863, 59879, 59887,
+ 59921, 59929, 59951, 59957, 59971, 59981, 59999, 60013, 60017, 60029,
+ 60037, 60041, 60077, 60083, 60089, 60091, 60101, 60103, 60107, 60127,
+ 60133, 60139, 60149, 60161, 60167, 60169, 60209, 60217, 60223, 60251,
+ 60257, 60259, 60271, 60289, 60293, 60317, 60331, 60337, 60343, 60353,
+ 60373, 60383, 60397, 60413, 60427, 60443, 60449, 60457, 60493, 60497,
+ 60509, 60521, 60527, 60539, 60589, 60601, 60607, 60611, 60617, 60623,
+ 60631, 60637, 60647, 60649, 60659, 60661, 60679, 60689, 60703, 60719,
+ 60727, 60733, 60737, 60757, 60761, 60763, 60773, 60779, 60793, 60811,
+ 60821, 60859, 60869, 60887, 60889, 60899, 60901, 60913, 60917, 60919,
+ 60923, 60937, 60943, 60953, 60961, 61001, 61007, 61027, 61031, 61043,
+ 61051, 61057, 61091, 61099, 61121, 61129, 61141, 61151, 61153, 61169,
+ 61211, 61223, 61231, 61253, 61261, 61283, 61291, 61297, 61331, 61333,
+ 61339, 61343, 61357, 61363, 61379, 61381, 61403, 61409, 61417, 61441,
+ 61463, 61469, 61471, 61483, 61487, 61493, 61507, 61511, 61519, 61543,
+ 61547, 61553, 61559, 61561, 61583, 61603, 61609, 61613, 61627, 61631,
+ 61637, 61643, 61651, 61657, 61667, 61673, 61681, 61687, 61703, 61717,
+ 61723, 61729, 61751, 61757, 61781, 61813, 61819, 61837, 61843, 61861,
+ 61871, 61879, 61909, 61927, 61933, 61949, 61961, 61967, 61979, 61981,
+ 61987, 61991, 62003, 62011, 62017, 62039, 62047, 62053, 62057, 62071,
+ 62081, 62099, 62119, 62129, 62131, 62137, 62141, 62143, 62171, 62189,
+ 62191, 62201, 62207, 62213, 62219, 62233, 62273, 62297, 62299, 62303,
+ 62311, 62323, 62327, 62347, 62351, 62383, 62401, 62417, 62423, 62459,
+ 62467, 62473, 62477, 62483, 62497, 62501, 62507, 62533, 62539, 62549,
+ 62563, 62581, 62591, 62597, 62603, 62617, 62627, 62633, 62639, 62653,
+ 62659, 62683, 62687, 62701, 62723, 62731, 62743, 62753, 62761, 62773,
+ 62791, 62801, 62819, 62827, 62851, 62861, 62869, 62873, 62897, 62903,
+ 62921, 62927, 62929, 62939, 62969, 62971, 62981, 62983, 62987, 62989,
+ 63029, 63031, 63059, 63067, 63073, 63079, 63097, 63103, 63113, 63127,
+ 63131, 63149, 63179, 63197, 63199, 63211, 63241, 63247, 63277, 63281,
+ 63299, 63311, 63313, 63317, 63331, 63337, 63347, 63353, 63361, 63367,
+ 63377, 63389, 63391, 63397, 63409, 63419, 63421, 63439, 63443, 63463,
+ 63467, 63473, 63487, 63493, 63499, 63521, 63527, 63533, 63541, 63559,
+ 63577, 63587, 63589, 63599, 63601, 63607, 63611, 63617, 63629, 63647,
+ 63649, 63659, 63667, 63671, 63689, 63691, 63697, 63703, 63709, 63719,
+ 63727, 63737, 63743, 63761, 63773, 63781, 63793, 63799, 63803, 63809,
+ 63823, 63839, 63841, 63853, 63857, 63863, 63901, 63907, 63913, 63929,
+ 63949, 63977, 63997, 64007, 64013, 64019, 64033, 64037, 64063, 64067,
+ 64081, 64091, 64109, 64123, 64151, 64153, 64157, 64171, 64187, 64189,
+ 64217, 64223, 64231, 64237, 64271, 64279, 64283, 64301, 64303, 64319,
+ 64327, 64333, 64373, 64381, 64399, 64403, 64433, 64439, 64451, 64453,
+ 64483, 64489, 64499, 64513, 64553, 64567, 64577, 64579, 64591, 64601,
+ 64609, 64613, 64621, 64627, 64633, 64661, 64663, 64667, 64679, 64693,
+ 64709, 64717, 64747, 64763, 64781, 64783, 64793, 64811, 64817, 64849,
+ 64853, 64871, 64877, 64879, 64891, 64901, 64919, 64921, 64927, 64937,
+ 64951, 64969, 64997, 65003, 65011, 65027, 65029, 65033, 65053, 65063,
+ 65071, 65089, 65099, 65101, 65111, 65119, 65123, 65129, 65141, 65147,
+ 65167, 65171, 65173, 65179, 65183, 65203, 65213, 65239, 65257, 65267,
+ 65269, 65287, 65293, 65309, 65323, 65327, 65353, 65357, 65371, 65381,
+ 65393, 65407, 65413, 65419, 65423, 65437, 65447, 65449, 65479, 65497,
+ 65519, 65521, 65537, 65539, 65543, 65551, 65557, 65563, 65579, 65581,
+ 65587, 65599, 65609, 65617, 65629, 65633, 65647, 65651, 65657, 65677,
+ 65687, 65699, 65701, 65707, 65713, 65717, 65719, 65729, 65731, 65761,
+ 65777, 65789, 65809, 65827, 65831, 65837, 65839, 65843, 65851, 65867,
+ 65881, 65899, 65921, 65927, 65929, 65951, 65957, 65963, 65981, 65983,
+ 65993, 66029, 66037, 66041, 66047, 66067, 66071, 66083, 66089, 66103,
+ 66107, 66109, 66137, 66161, 66169, 66173, 66179, 66191, 66221, 66239,
+ 66271, 66293, 66301, 66337, 66343, 66347, 66359, 66361, 66373, 66377,
+ 66383, 66403, 66413, 66431, 66449, 66457, 66463, 66467, 66491, 66499,
+ 66509, 66523, 66529, 66533, 66541, 66553, 66569, 66571, 66587, 66593,
+ 66601, 66617, 66629, 66643, 66653, 66683, 66697, 66701, 66713, 66721,
+ 66733, 66739, 66749, 66751, 66763, 66791, 66797, 66809, 66821, 66841,
+ 66851, 66853, 66863, 66877, 66883, 66889, 66919, 66923, 66931, 66943,
+ 66947, 66949, 66959, 66973, 66977, 67003, 67021, 67033, 67043, 67049,
+ 67057, 67061, 67073, 67079, 67103, 67121, 67129, 67139, 67141, 67153,
+ 67157, 67169, 67181, 67187, 67189, 67211, 67213, 67217, 67219, 67231,
+ 67247, 67261, 67271, 67273, 67289, 67307, 67339, 67343, 67349, 67369,
+ 67391, 67399, 67409, 67411, 67421, 67427, 67429, 67433, 67447, 67453,
+ 67477, 67481, 67489, 67493, 67499, 67511, 67523, 67531, 67537, 67547,
+ 67559, 67567, 67577, 67579, 67589, 67601, 67607, 67619, 67631, 67651,
+ 67679, 67699, 67709, 67723, 67733, 67741, 67751, 67757, 67759, 67763,
+ 67777, 67783, 67789, 67801, 67807, 67819, 67829, 67843, 67853, 67867,
+ 67883, 67891, 67901, 67927, 67931, 67933, 67939, 67943, 67957, 67961,
+ 67967, 67979, 67987, 67993, 68023, 68041, 68053, 68059, 68071, 68087,
+ 68099, 68111, 68113, 68141, 68147, 68161, 68171, 68207, 68209, 68213,
+ 68219, 68227, 68239, 68261, 68279, 68281, 68311, 68329, 68351, 68371,
+ 68389, 68399, 68437, 68443, 68447, 68449, 68473, 68477, 68483, 68489,
+ 68491, 68501, 68507, 68521, 68531, 68539, 68543, 68567, 68581, 68597,
+ 68611, 68633, 68639, 68659, 68669, 68683, 68687, 68699, 68711, 68713,
+ 68729, 68737, 68743, 68749, 68767, 68771, 68777, 68791, 68813, 68819,
+ 68821, 68863, 68879, 68881, 68891, 68897, 68899, 68903, 68909, 68917,
+ 68927, 68947, 68963, 68993, 69001, 69011, 69019, 69029, 69031, 69061,
+ 69067, 69073, 69109, 69119, 69127, 69143, 69149, 69151, 69163, 69191,
+ 69193, 69197, 69203, 69221, 69233, 69239, 69247, 69257, 69259, 69263,
+ 69313, 69317, 69337, 69341, 69371, 69379, 69383, 69389, 69401, 69403,
+ 69427, 69431, 69439, 69457, 69463, 69467, 69473, 69481, 69491, 69493,
+ 69497, 69499, 69539, 69557, 69593, 69623, 69653, 69661, 69677, 69691,
+ 69697, 69709, 69737, 69739, 69761, 69763, 69767, 69779, 69809, 69821,
+ 69827, 69829, 69833, 69847, 69857, 69859, 69877, 69899, 69911, 69929,
+ 69931, 69941, 69959, 69991, 69997, 70001, 70003, 70009, 70019, 70039,
+ 70051, 70061, 70067, 70079, 70099, 70111, 70117, 70121, 70123, 70139,
+ 70141, 70157, 70163, 70177, 70181, 70183, 70199, 70201, 70207, 70223,
+ 70229, 70237, 70241, 70249, 70271, 70289, 70297, 70309, 70313, 70321,
+ 70327, 70351, 70373, 70379, 70381, 70393, 70423, 70429, 70439, 70451,
+ 70457, 70459, 70481, 70487, 70489, 70501, 70507, 70529, 70537, 70549,
+ 70571, 70573, 70583, 70589, 70607, 70619, 70621, 70627, 70639, 70657,
+ 70663, 70667, 70687, 70709, 70717, 70729, 70753, 70769, 70783, 70793,
+ 70823, 70841, 70843, 70849, 70853, 70867, 70877, 70879, 70891, 70901,
+ 70913, 70919, 70921, 70937, 70949, 70951, 70957, 70969, 70979, 70981,
+ 70991, 70997, 70999, 71011, 71023, 71039, 71059, 71069, 71081, 71089,
+ 71119, 71129, 71143, 71147, 71153, 71161, 71167, 71171, 71191, 71209,
+ 71233, 71237, 71249, 71257, 71261, 71263, 71287, 71293, 71317, 71327,
+ 71329, 71333, 71339, 71341, 71347, 71353, 71359, 71363, 71387, 71389,
+ 71399, 71411, 71413, 71419, 71429, 71437, 71443, 71453, 71471, 71473,
+ 71479, 71483, 71503, 71527, 71537, 71549, 71551, 71563, 71569, 71593,
+ 71597, 71633, 71647, 71663, 71671, 71693, 71699, 71707, 71711, 71713,
+ 71719, 71741, 71761, 71777, 71789, 71807, 71809, 71821, 71837, 71843,
+ 71849, 71861, 71867, 71879, 71881, 71887, 71899, 71909, 71917, 71933,
+ 71941, 71947, 71963, 71971, 71983, 71987, 71993, 71999, 72019, 72031,
+ 72043, 72047, 72053, 72073, 72077, 72089, 72091, 72101, 72103, 72109,
+ 72139, 72161, 72167, 72169, 72173, 72211, 72221, 72223, 72227, 72229,
+ 72251, 72253, 72269, 72271, 72277, 72287, 72307, 72313, 72337, 72341,
+ 72353, 72367, 72379, 72383, 72421, 72431, 72461, 72467, 72469, 72481,
+ 72493, 72497, 72503, 72533, 72547, 72551, 72559, 72577, 72613, 72617,
+ 72623, 72643, 72647, 72649, 72661, 72671, 72673, 72679, 72689, 72701,
+ 72707, 72719, 72727, 72733, 72739, 72763, 72767, 72797, 72817, 72823,
+ 72859, 72869, 72871, 72883, 72889, 72893, 72901, 72907, 72911, 72923,
+ 72931, 72937, 72949, 72953, 72959, 72973, 72977, 72997, 73009, 73013,
+ 73019, 73037, 73039, 73043, 73061, 73063, 73079, 73091, 73121, 73127,
+ 73133, 73141, 73181, 73189, 73237, 73243, 73259, 73277, 73291, 73303,
+ 73309, 73327, 73331, 73351, 73361, 73363, 73369, 73379, 73387, 73417,
+ 73421, 73433, 73453, 73459, 73471, 73477, 73483, 73517, 73523, 73529,
+ 73547, 73553, 73561, 73571, 73583, 73589, 73597, 73607, 73609, 73613,
+ 73637, 73643, 73651, 73673, 73679, 73681, 73693, 73699, 73709, 73721,
+ 73727, 73751, 73757, 73771, 73783, 73819, 73823, 73847, 73849, 73859,
+ 73867, 73877, 73883, 73897, 73907, 73939, 73943, 73951, 73961, 73973,
+ 73999, 74017, 74021, 74027, 74047, 74051, 74071, 74077, 74093, 74099,
+ 74101, 74131, 74143, 74149, 74159, 74161, 74167, 74177, 74189, 74197,
+ 74201, 74203, 74209, 74219, 74231, 74257, 74279, 74287, 74293, 74297,
+ 74311, 74317, 74323, 74353, 74357, 74363, 74377, 74381, 74383, 74411,
+ 74413, 74419, 74441, 74449, 74453, 74471, 74489, 74507, 74509, 74521,
+ 74527, 74531, 74551, 74561, 74567, 74573, 74587, 74597, 74609, 74611,
+ 74623, 74653, 74687, 74699, 74707, 74713, 74717, 74719, 74729, 74731,
+ 74747, 74759, 74761, 74771, 74779, 74797, 74821, 74827, 74831, 74843,
+ 74857, 74861, 74869, 74873, 74887, 74891, 74897, 74903, 74923, 74929,
+ 74933, 74941, 74959, 75011, 75013, 75017, 75029, 75037, 75041, 75079,
+ 75083, 75109, 75133, 75149, 75161, 75167, 75169, 75181, 75193, 75209,
+ 75211, 75217, 75223, 75227, 75239, 75253, 75269, 75277, 75289, 75307,
+ 75323, 75329, 75337, 75347, 75353, 75367, 75377, 75389, 75391, 75401,
+ 75403, 75407, 75431, 75437, 75479, 75503, 75511, 75521, 75527, 75533,
+ 75539, 75541, 75553, 75557, 75571, 75577, 75583, 75611, 75617, 75619,
+ 75629, 75641, 75653, 75659, 75679, 75683, 75689, 75703, 75707, 75709,
+ 75721, 75731, 75743, 75767, 75773, 75781, 75787, 75793, 75797, 75821,
+ 75833, 75853, 75869, 75883, 75913, 75931, 75937, 75941, 75967, 75979,
+ 75983, 75989, 75991, 75997, 76001, 76003, 76031, 76039, 76079, 76081,
+ 76091, 76099, 76103, 76123, 76129, 76147, 76157, 76159, 76163, 76207,
+ 76213, 76231, 76243, 76249, 76253, 76259, 76261, 76283, 76289, 76303,
+ 76333, 76343, 76367, 76369, 76379, 76387, 76403, 76421, 76423, 76441,
+ 76463, 76471, 76481, 76487, 76493, 76507, 76511, 76519, 76537, 76541,
+ 76543, 76561, 76579, 76597, 76603, 76607, 76631, 76649, 76651, 76667,
+ 76673, 76679, 76697, 76717, 76733, 76753, 76757, 76771, 76777, 76781,
+ 76801, 76819, 76829, 76831, 76837, 76847, 76871, 76873, 76883, 76907,
+ 76913, 76919, 76943, 76949, 76961, 76963, 76991, 77003, 77017, 77023,
+ 77029, 77041, 77047, 77069, 77081, 77093, 77101, 77137, 77141, 77153,
+ 77167, 77171, 77191, 77201, 77213, 77237, 77239, 77243, 77249, 77261,
+ 77263, 77267, 77269, 77279, 77291, 77317, 77323, 77339, 77347, 77351,
+ 77359, 77369, 77377, 77383, 77417, 77419, 77431, 77447, 77471, 77477,
+ 77479, 77489, 77491, 77509, 77513, 77521, 77527, 77543, 77549, 77551,
+ 77557, 77563, 77569, 77573, 77587, 77591, 77611, 77617, 77621, 77641,
+ 77647, 77659, 77681, 77687, 77689, 77699, 77711, 77713, 77719, 77723,
+ 77731, 77743, 77747, 77761, 77773, 77783, 77797, 77801, 77813, 77839,
+ 77849, 77863, 77867, 77893, 77899, 77929, 77933, 77951, 77969, 77977,
+ 77983, 77999, 78007, 78017, 78031, 78041, 78049, 78059, 78079, 78101,
+ 78121, 78137, 78139, 78157, 78163, 78167, 78173, 78179, 78191, 78193,
+ 78203, 78229, 78233, 78241, 78259, 78277, 78283, 78301, 78307, 78311,
+ 78317, 78341, 78347, 78367, 78401, 78427, 78437, 78439, 78467, 78479,
+ 78487, 78497, 78509, 78511, 78517, 78539, 78541, 78553, 78569, 78571,
+ 78577, 78583, 78593, 78607, 78623, 78643, 78649, 78653, 78691, 78697,
+ 78707, 78713, 78721, 78737, 78779, 78781, 78787, 78791, 78797, 78803,
+ 78809, 78823, 78839, 78853, 78857, 78877, 78887, 78889, 78893, 78901,
+ 78919, 78929, 78941, 78977, 78979, 78989, 79031, 79039, 79043, 79063,
+ 79087, 79103, 79111, 79133, 79139, 79147, 79151, 79153, 79159, 79181,
+ 79187, 79193, 79201, 79229, 79231, 79241, 79259, 79273, 79279, 79283,
+ 79301, 79309, 79319, 79333, 79337, 79349, 79357, 79367, 79379, 79393,
+ 79397, 79399, 79411, 79423, 79427, 79433, 79451, 79481, 79493, 79531,
+ 79537, 79549, 79559, 79561, 79579, 79589, 79601, 79609, 79613, 79621,
+ 79627, 79631, 79633, 79657, 79669, 79687, 79691, 79693, 79697, 79699,
+ 79757, 79769, 79777, 79801, 79811, 79813, 79817, 79823, 79829, 79841,
+ 79843, 79847, 79861, 79867, 79873, 79889, 79901, 79903, 79907, 79939,
+ 79943, 79967, 79973, 79979, 79987, 79997, 79999, 80021, 80039, 80051,
+ 80071, 80077, 80107, 80111, 80141, 80147, 80149, 80153, 80167, 80173,
+ 80177, 80191, 80207, 80209, 80221, 80231, 80233, 80239, 80251, 80263,
+ 80273, 80279, 80287, 80309, 80317, 80329, 80341, 80347, 80363, 80369,
+ 80387, 80407, 80429, 80447, 80449, 80471, 80473, 80489, 80491, 80513,
+ 80527, 80537, 80557, 80567, 80599, 80603, 80611, 80621, 80627, 80629,
+ 80651, 80657, 80669, 80671, 80677, 80681, 80683, 80687, 80701, 80713,
+ 80737, 80747, 80749, 80761, 80777, 80779, 80783, 80789, 80803, 80809,
+ 80819, 80831, 80833, 80849, 80863, 80897, 80909, 80911, 80917, 80923,
+ 80929, 80933, 80953, 80963, 80989, 81001, 81013, 81017, 81019, 81023,
+ 81031, 81041, 81043, 81047, 81049, 81071, 81077, 81083, 81097, 81101,
+ 81119, 81131, 81157, 81163, 81173, 81181, 81197, 81199, 81203, 81223,
+ 81233, 81239, 81281, 81283, 81293, 81299, 81307, 81331, 81343, 81349,
+ 81353, 81359, 81371, 81373, 81401, 81409, 81421, 81439, 81457, 81463,
+ 81509, 81517, 81527, 81533, 81547, 81551, 81553, 81559, 81563, 81569,
+ 81611, 81619, 81629, 81637, 81647, 81649, 81667, 81671, 81677, 81689,
+ 81701, 81703, 81707, 81727, 81737, 81749, 81761, 81769, 81773, 81799,
+ 81817, 81839, 81847, 81853, 81869, 81883, 81899, 81901, 81919, 81929,
+ 81931, 81937, 81943, 81953, 81967, 81971, 81973, 82003, 82007, 82009,
+ 82013, 82021, 82031, 82037, 82039, 82051, 82067, 82073, 82129, 82139,
+ 82141, 82153, 82163, 82171, 82183, 82189, 82193, 82207, 82217, 82219,
+ 82223, 82231, 82237, 82241, 82261, 82267, 82279, 82301, 82307, 82339,
+ 82349, 82351, 82361, 82373, 82387, 82393, 82421, 82457, 82463, 82469,
+ 82471, 82483, 82487, 82493, 82499, 82507, 82529, 82531, 82549, 82559,
+ 82561, 82567, 82571, 82591, 82601, 82609, 82613, 82619, 82633, 82651,
+ 82657, 82699, 82721, 82723, 82727, 82729, 82757, 82759, 82763, 82781,
+ 82787, 82793, 82799, 82811, 82813, 82837, 82847, 82883, 82889, 82891,
+ 82903, 82913, 82939, 82963, 82981, 82997, 83003, 83009, 83023, 83047,
+ 83059, 83063, 83071, 83077, 83089, 83093, 83101, 83117, 83137, 83177,
+ 83203, 83207, 83219, 83221, 83227, 83231, 83233, 83243, 83257, 83267,
+ 83269, 83273, 83299, 83311, 83339, 83341, 83357, 83383, 83389, 83399,
+ 83401, 83407, 83417, 83423, 83431, 83437, 83443, 83449, 83459, 83471,
+ 83477, 83497, 83537, 83557, 83561, 83563, 83579, 83591, 83597, 83609,
+ 83617, 83621, 83639, 83641, 83653, 83663, 83689, 83701, 83717, 83719,
+ 83737, 83761, 83773, 83777, 83791, 83813, 83833, 83843, 83857, 83869,
+ 83873, 83891, 83903, 83911, 83921, 83933, 83939, 83969, 83983, 83987,
+ 84011, 84017, 84047, 84053, 84059, 84061, 84067, 84089, 84121, 84127,
+ 84131, 84137, 84143, 84163, 84179, 84181, 84191, 84199, 84211, 84221,
+ 84223, 84229, 84239, 84247, 84263, 84299, 84307, 84313, 84317, 84319,
+ 84347, 84349, 84377, 84389, 84391, 84401, 84407, 84421, 84431, 84437,
+ 84443, 84449, 84457, 84463, 84467, 84481, 84499, 84503, 84509, 84521,
+ 84523, 84533, 84551, 84559, 84589, 84629, 84631, 84649, 84653, 84659,
+ 84673, 84691, 84697, 84701, 84713, 84719, 84731, 84737, 84751, 84761,
+ 84787, 84793, 84809, 84811, 84827, 84857, 84859, 84869, 84871, 84913,
+ 84919, 84947, 84961, 84967, 84977, 84979, 84991, 85009, 85021, 85027,
+ 85037, 85049, 85061, 85081, 85087, 85091, 85093, 85103, 85109, 85121,
+ 85133, 85147, 85159, 85193, 85199, 85201, 85213, 85223, 85229, 85237,
+ 85243, 85247, 85259, 85297, 85303, 85313, 85331, 85333, 85361, 85363,
+ 85369, 85381, 85411, 85427, 85429, 85439, 85447, 85451, 85453, 85469,
+ 85487, 85513, 85517, 85523, 85531, 85549, 85571, 85577, 85597, 85601,
+ 85607, 85619, 85621, 85627, 85639, 85643, 85661, 85667, 85669, 85691,
+ 85703, 85711, 85717, 85733, 85751, 85781, 85793, 85817, 85819, 85829,
+ 85831, 85837, 85843, 85847, 85853, 85889, 85903, 85909, 85931, 85933,
+ 85991, 85999, 86011, 86017, 86027, 86029, 86069, 86077, 86083, 86111,
+ 86113, 86117, 86131, 86137, 86143, 86161, 86171, 86179, 86183, 86197,
+ 86201, 86209, 86239, 86243, 86249, 86257, 86263, 86269, 86287, 86291,
+ 86293, 86297, 86311, 86323, 86341, 86351, 86353, 86357, 86369, 86371,
+ 86381, 86389, 86399, 86413, 86423, 86441, 86453, 86461, 86467, 86477,
+ 86491, 86501, 86509, 86531, 86533, 86539, 86561, 86573, 86579, 86587,
+ 86599, 86627, 86629, 86677, 86689, 86693, 86711, 86719, 86729, 86743,
+ 86753, 86767, 86771, 86783, 86813, 86837, 86843, 86851, 86857, 86861,
+ 86869, 86923, 86927, 86929, 86939, 86951, 86959, 86969, 86981, 86993,
+ 87011, 87013, 87037, 87041, 87049, 87071, 87083, 87103, 87107, 87119,
+ 87121, 87133, 87149, 87151, 87179, 87181, 87187, 87211, 87221, 87223,
+ 87251, 87253, 87257, 87277, 87281, 87293, 87299, 87313, 87317, 87323,
+ 87337, 87359, 87383, 87403, 87407, 87421, 87427, 87433, 87443, 87473,
+ 87481, 87491, 87509, 87511, 87517, 87523, 87539, 87541, 87547, 87553,
+ 87557, 87559, 87583, 87587, 87589, 87613, 87623, 87629, 87631, 87641,
+ 87643, 87649, 87671, 87679, 87683, 87691, 87697, 87701, 87719, 87721,
+ 87739, 87743, 87751, 87767, 87793, 87797, 87803, 87811, 87833, 87853,
+ 87869, 87877, 87881, 87887, 87911, 87917, 87931, 87943, 87959, 87961,
+ 87973, 87977, 87991, 88001, 88003, 88007, 88019, 88037, 88069, 88079,
+ 88093, 88117, 88129, 88169, 88177, 88211, 88223, 88237, 88241, 88259,
+ 88261, 88289, 88301, 88321, 88327, 88337, 88339, 88379, 88397, 88411,
+ 88423, 88427, 88463, 88469, 88471, 88493, 88499, 88513, 88523, 88547,
+ 88589, 88591, 88607, 88609, 88643, 88651, 88657, 88661, 88663, 88667,
+ 88681, 88721, 88729, 88741, 88747, 88771, 88789, 88793, 88799, 88801,
+ 88807, 88811, 88813, 88817, 88819, 88843, 88853, 88861, 88867, 88873,
+ 88883, 88897, 88903, 88919, 88937, 88951, 88969, 88993, 88997, 89003,
+ 89009, 89017, 89021, 89041, 89051, 89057, 89069, 89071, 89083, 89087,
+ 89101, 89107, 89113, 89119, 89123, 89137, 89153, 89189, 89203, 89209,
+ 89213, 89227, 89231, 89237, 89261, 89269, 89273, 89293, 89303, 89317,
+ 89329, 89363, 89371, 89381, 89387, 89393, 89399, 89413, 89417, 89431,
+ 89443, 89449, 89459, 89477, 89491, 89501, 89513, 89519, 89521, 89527,
+ 89533, 89561, 89563, 89567, 89591, 89597, 89599, 89603, 89611, 89627,
+ 89633, 89653, 89657, 89659, 89669, 89671, 89681, 89689, 89753, 89759,
+ 89767, 89779, 89783, 89797, 89809, 89819, 89821, 89833, 89839, 89849,
+ 89867, 89891, 89897, 89899, 89909, 89917, 89923, 89939, 89959, 89963,
+ 89977, 89983, 89989, 90001, 90007, 90011, 90017, 90019, 90023, 90031,
+ 90053, 90059, 90067, 90071, 90073, 90089, 90107, 90121, 90127, 90149,
+ 90163, 90173, 90187, 90191, 90197, 90199, 90203, 90217, 90227, 90239,
+ 90247, 90263, 90271, 90281, 90289, 90313, 90353, 90359, 90371, 90373,
+ 90379, 90397, 90401, 90403, 90407, 90437, 90439, 90469, 90473, 90481,
+ 90499, 90511, 90523, 90527, 90529, 90533, 90547, 90583, 90599, 90617,
+ 90619, 90631, 90641, 90647, 90659, 90677, 90679, 90697, 90703, 90709,
+ 90731, 90749, 90787, 90793, 90803, 90821, 90823, 90833, 90841, 90847,
+ 90863, 90887, 90901, 90907, 90911, 90917, 90931, 90947, 90971, 90977,
+ 90989, 90997, 91009, 91019, 91033, 91079, 91081, 91097, 91099, 91121,
+ 91127, 91129, 91139, 91141, 91151, 91153, 91159, 91163, 91183, 91193,
+ 91199, 91229, 91237, 91243, 91249, 91253, 91283, 91291, 91297, 91303,
+ 91309, 91331, 91367, 91369, 91373, 91381, 91387, 91393, 91397, 91411,
+ 91423, 91433, 91453, 91457, 91459, 91463, 91493, 91499, 91513, 91529,
+ 91541, 91571, 91573, 91577, 91583, 91591, 91621, 91631, 91639, 91673,
+ 91691, 91703, 91711, 91733, 91753, 91757, 91771, 91781, 91801, 91807,
+ 91811, 91813, 91823, 91837, 91841, 91867, 91873, 91909, 91921, 91939,
+ 91943, 91951, 91957, 91961, 91967, 91969, 91997, 92003, 92009, 92033,
+ 92041, 92051, 92077, 92083, 92107, 92111, 92119, 92143, 92153, 92173,
+ 92177, 92179, 92189, 92203, 92219, 92221, 92227, 92233, 92237, 92243,
+ 92251, 92269, 92297, 92311, 92317, 92333, 92347, 92353, 92357, 92363,
+ 92369, 92377, 92381, 92383, 92387, 92399, 92401, 92413, 92419, 92431,
+ 92459, 92461, 92467, 92479, 92489, 92503, 92507, 92551, 92557, 92567,
+ 92569, 92581, 92593, 92623, 92627, 92639, 92641, 92647, 92657, 92669,
+ 92671, 92681, 92683, 92693, 92699, 92707, 92717, 92723, 92737, 92753,
+ 92761, 92767, 92779, 92789, 92791, 92801, 92809, 92821, 92831, 92849,
+ 92857, 92861, 92863, 92867, 92893, 92899, 92921, 92927, 92941, 92951,
+ 92957, 92959, 92987, 92993, 93001, 93047, 93053, 93059, 93077, 93083,
+ 93089, 93097, 93103, 93113, 93131, 93133, 93139, 93151, 93169, 93179,
+ 93187, 93199, 93229, 93239, 93241, 93251, 93253, 93257, 93263, 93281,
+ 93283, 93287, 93307, 93319, 93323, 93329, 93337, 93371, 93377, 93383,
+ 93407, 93419, 93427, 93463, 93479, 93481, 93487, 93491, 93493, 93497,
+ 93503, 93523, 93529, 93553, 93557, 93559, 93563, 93581, 93601, 93607,
+ 93629, 93637, 93683, 93701, 93703, 93719, 93739, 93761, 93763, 93787,
+ 93809, 93811, 93827, 93851, 93871, 93887, 93889, 93893, 93901, 93911,
+ 93913, 93923, 93937, 93941, 93949, 93967, 93971, 93979, 93983, 93997,
+ 94007, 94009, 94033, 94049, 94057, 94063, 94079, 94099, 94109, 94111,
+ 94117, 94121, 94151, 94153, 94169, 94201, 94207, 94219, 94229, 94253,
+ 94261, 94273, 94291, 94307, 94309, 94321, 94327, 94331, 94343, 94349,
+ 94351, 94379, 94397, 94399, 94421, 94427, 94433, 94439, 94441, 94447,
+ 94463, 94477, 94483, 94513, 94529, 94531, 94541, 94543, 94547, 94559,
+ 94561, 94573, 94583, 94597, 94603, 94613, 94621, 94649, 94651, 94687,
+ 94693, 94709, 94723, 94727, 94747, 94771, 94777, 94781, 94789, 94793,
+ 94811, 94819, 94823, 94837, 94841, 94847, 94849, 94873, 94889, 94903,
+ 94907, 94933, 94949, 94951, 94961, 94993, 94999, 95003, 95009, 95021,
+ 95027, 95063, 95071, 95083, 95087, 95089, 95093, 95101, 95107, 95111,
+ 95131, 95143, 95153, 95177, 95189, 95191, 95203, 95213, 95219, 95231,
+ 95233, 95239, 95257, 95261, 95267, 95273, 95279, 95287, 95311, 95317,
+ 95327, 95339, 95369, 95383, 95393, 95401, 95413, 95419, 95429, 95441,
+ 95443, 95461, 95467, 95471, 95479, 95483, 95507, 95527, 95531, 95539,
+ 95549, 95561, 95569, 95581, 95597, 95603, 95617, 95621, 95629, 95633,
+ 95651, 95701, 95707, 95713, 95717, 95723, 95731, 95737, 95747, 95773,
+ 95783, 95789, 95791, 95801, 95803, 95813, 95819, 95857, 95869, 95873,
+ 95881, 95891, 95911, 95917, 95923, 95929, 95947, 95957, 95959, 95971,
+ 95987, 95989, 96001, 96013, 96017, 96043, 96053, 96059, 96079, 96097,
+ 96137, 96149, 96157, 96167, 96179, 96181, 96199, 96211, 96221, 96223,
+ 96233, 96259, 96263, 96269, 96281, 96289, 96293, 96323, 96329, 96331,
+ 96337, 96353, 96377, 96401, 96419, 96431, 96443, 96451, 96457, 96461,
+ 96469, 96479, 96487, 96493, 96497, 96517, 96527, 96553, 96557, 96581,
+ 96587, 96589, 96601, 96643, 96661, 96667, 96671, 96697, 96703, 96731,
+ 96737, 96739, 96749, 96757, 96763, 96769, 96779, 96787, 96797, 96799,
+ 96821, 96823, 96827, 96847, 96851, 96857, 96893, 96907, 96911, 96931,
+ 96953, 96959, 96973, 96979, 96989, 96997, 97001, 97003, 97007, 97021,
+ 97039, 97073, 97081, 97103, 97117, 97127, 97151, 97157, 97159, 97169,
+ 97171, 97177, 97187, 97213, 97231, 97241, 97259, 97283, 97301, 97303,
+ 97327, 97367, 97369, 97373, 97379, 97381, 97387, 97397, 97423, 97429,
+ 97441, 97453, 97459, 97463, 97499, 97501, 97511, 97523, 97547, 97549,
+ 97553, 97561, 97571, 97577, 97579, 97583, 97607, 97609, 97613, 97649,
+ 97651, 97673, 97687, 97711, 97729, 97771, 97777, 97787, 97789, 97813,
+ 97829, 97841, 97843, 97847, 97849, 97859, 97861, 97871, 97879, 97883,
+ 97919, 97927, 97931, 97943, 97961, 97967, 97973, 97987, 98009, 98011,
+ 98017, 98041, 98047, 98057, 98081, 98101, 98123, 98129, 98143, 98179,
+ 98207, 98213, 98221, 98227, 98251, 98257, 98269, 98297, 98299, 98317,
+ 98321, 98323, 98327, 98347, 98369, 98377, 98387, 98389, 98407, 98411,
+ 98419, 98429, 98443, 98453, 98459, 98467, 98473, 98479, 98491, 98507,
+ 98519, 98533, 98543, 98561, 98563, 98573, 98597, 98621, 98627, 98639,
+ 98641, 98663, 98669, 98689, 98711, 98713, 98717, 98729, 98731, 98737,
+ 98773, 98779, 98801, 98807, 98809, 98837, 98849, 98867, 98869, 98873,
+ 98887, 98893, 98897, 98899, 98909, 98911, 98927, 98929, 98939, 98947,
+ 98953, 98963, 98981, 98993, 98999, 99013, 99017, 99023, 99041, 99053,
+ 99079, 99083, 99089, 99103, 99109, 99119, 99131, 99133, 99137, 99139,
+ 99149, 99173, 99181, 99191, 99223, 99233, 99241, 99251, 99257, 99259,
+ 99277, 99289, 99317, 99347, 99349, 99367, 99371, 99377, 99391, 99397,
+ 99401, 99409, 99431, 99439, 99469, 99487, 99497, 99523, 99527, 99529,
+ 99551, 99559, 99563, 99571, 99577, 99581, 99607, 99611, 99623, 99643,
+ 99661, 99667, 99679, 99689, 99707, 99709, 99713, 99719, 99721, 99733,
+ 99761, 99767, 99787, 99793, 99809, 99817, 99823, 99829, 99833, 99839,
+ 99859, 99871, 99877, 99881, 99901, 99907, 99923, 99929, 99961, 99971,
+ 99989, 99991, 100003, 100019, 100043, 100049, 100057, 100069, 100103, 100109,
+100129, 100151, 100153, 100169, 100183, 100189, 100193, 100207, 100213, 100237,
+100267, 100271, 100279, 100291, 100297, 100313, 100333, 100343, 100357, 100361,
+100363, 100379, 100391, 100393, 100403, 100411, 100417, 100447, 100459, 100469,
+100483, 100493, 100501, 100511, 100517, 100519, 100523, 100537, 100547, 100549,
+100559, 100591, 100609, 100613, 100621, 100649, 100669, 100673, 100693, 100699,
+100703, 100733, 100741, 100747, 100769, 100787, 100799, 100801, 100811, 100823,
+100829, 100847, 100853, 100907, 100913, 100927, 100931, 100937, 100943, 100957,
+100981, 100987, 100999, 101009, 101021, 101027, 101051, 101063, 101081, 101089,
+101107, 101111, 101113, 101117, 101119, 101141, 101149, 101159, 101161, 101173,
+101183, 101197, 101203, 101207, 101209, 101221, 101267, 101273, 101279, 101281,
+101287, 101293, 101323, 101333, 101341, 101347, 101359, 101363, 101377, 101383,
+101399, 101411, 101419, 101429, 101449, 101467, 101477, 101483, 101489, 101501,
+101503, 101513, 101527, 101531, 101533, 101537, 101561, 101573, 101581, 101599,
+101603, 101611, 101627, 101641, 101653, 101663, 101681, 101693, 101701, 101719,
+101723, 101737, 101741, 101747, 101749, 101771, 101789, 101797, 101807, 101833,
+101837, 101839, 101863, 101869, 101873, 101879, 101891, 101917, 101921, 101929,
+101939, 101957, 101963, 101977, 101987, 101999, 102001, 102013, 102019, 102023,
+102031, 102043, 102059, 102061, 102071, 102077, 102079, 102101, 102103, 102107,
+102121, 102139, 102149, 102161, 102181, 102191, 102197, 102199, 102203, 102217,
+102229, 102233, 102241, 102251, 102253, 102259, 102293, 102299, 102301, 102317,
+102329, 102337, 102359, 102367, 102397, 102407, 102409, 102433, 102437, 102451,
+102461, 102481, 102497, 102499, 102503, 102523, 102533, 102539, 102547, 102551,
+102559, 102563, 102587, 102593, 102607, 102611, 102643, 102647, 102653, 102667,
+102673, 102677, 102679, 102701, 102761, 102763, 102769, 102793, 102797, 102811,
+102829, 102841, 102859, 102871, 102877, 102881, 102911, 102913, 102929, 102931,
+102953, 102967, 102983, 103001, 103007, 103043, 103049, 103067, 103069, 103079,
+103087, 103091, 103093, 103099, 103123, 103141, 103171, 103177, 103183, 103217,
+103231, 103237, 103289, 103291, 103307, 103319, 103333, 103349, 103357, 103387,
+103391, 103393, 103399, 103409, 103421, 103423, 103451, 103457, 103471, 103483,
+103511, 103529, 103549, 103553, 103561, 103567, 103573, 103577, 103583, 103591,
+103613, 103619, 103643, 103651, 103657, 103669, 103681, 103687, 103699, 103703,
+103723, 103769, 103787, 103801, 103811, 103813, 103837, 103841, 103843, 103867,
+103889, 103903, 103913, 103919, 103951, 103963, 103967, 103969, 103979, 103981,
+103991, 103993, 103997, 104003, 104009, 104021, 104033, 104047, 104053, 104059,
+104087, 104089, 104107, 104113, 104119, 104123, 104147, 104149, 104161, 104173,
+104179, 104183, 104207, 104231, 104233, 104239, 104243, 104281, 104287, 104297,
+104309, 104311, 104323, 104327, 104347, 104369, 104381, 104383, 104393, 104399,
+104417, 104459, 104471, 104473, 104479, 104491, 104513, 104527, 104537, 104543,
+104549, 104551, 104561, 104579, 104593, 104597, 104623, 104639, 104651, 104659,
+104677, 104681, 104683, 104693, 104701, 104707, 104711, 104717, 104723, 104729,
+)
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/number.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Util/number.pyi
new file mode 100644
index 00000000..f8680bf8
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/number.pyi
@@ -0,0 +1,19 @@
+from typing import List, Optional, Callable
+
+
+def ceil_div(n: int, d: int) -> int: ...
+def size (N: int) -> int: ...
+def getRandomInteger(N: int, randfunc: Optional[Callable]=None) -> int: ...
+def getRandomRange(a: int, b: int, randfunc: Optional[Callable]=None) -> int: ...
+def getRandomNBitInteger(N: int, randfunc: Optional[Callable]=None) -> int: ...
+def GCD(x: int,y: int) -> int: ...
+def inverse(u: int, v: int) -> int: ...
+def getPrime(N: int, randfunc: Optional[Callable]=None) -> int: ...
+def getStrongPrime(N: int, e: Optional[int]=0, false_positive_prob: Optional[float]=1e-6, randfunc: Optional[Callable]=None) -> int: ...
+def isPrime(N: int, false_positive_prob: Optional[float]=1e-6, randfunc: Optional[Callable]=None) -> bool: ...
+def long_to_bytes(n: int, blocksize: Optional[int]=0) -> bytes: ...
+def bytes_to_long(s: bytes) -> int: ...
+def long2str(n: int, blocksize: Optional[int]=0) -> bytes: ...
+def str2long(s: bytes) -> int: ...
+
+sieve_base: List[int]
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/py3compat.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/py3compat.py
new file mode 100644
index 00000000..087550d4
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/py3compat.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+#
+# Util/py3compat.py : Compatibility code for handling Py3k / Python 2.x
+#
+# Written in 2010 by Thorsten Behrens
+#
+# ===================================================================
+# 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.
+# ===================================================================
+
+"""Compatibility code for handling string/bytes changes from Python 2.x to Py3k
+
+In Python 2.x, strings (of type ''str'') contain binary data, including encoded
+Unicode text (e.g. UTF-8). The separate type ''unicode'' holds Unicode text.
+Unicode literals are specified via the u'...' prefix. Indexing or slicing
+either type always produces a string of the same type as the original.
+Data read from a file is always of '''str'' type.
+
+In Python 3.x, strings (type ''str'') may only contain Unicode text. The u'...'
+prefix and the ''unicode'' type are now redundant. A new type (called
+''bytes'') has to be used for binary data (including any particular
+''encoding'' of a string). The b'...' prefix allows one to specify a binary
+literal. Indexing or slicing a string produces another string. Slicing a byte
+string produces another byte string, but the indexing operation produces an
+integer. Data read from a file is of '''str'' type if the file was opened in
+text mode, or of ''bytes'' type otherwise.
+
+Since PyCrypto aims at supporting both Python 2.x and 3.x, the following helper
+functions are used to keep the rest of the library as independent as possible
+from the actual Python version.
+
+In general, the code should always deal with binary strings, and use integers
+instead of 1-byte character strings.
+
+b(s)
+ Take a text string literal (with no prefix or with u'...' prefix) and
+ make a byte string.
+bchr(c)
+ Take an integer and make a 1-character byte string.
+bord(c)
+ Take the result of indexing on a byte string and make an integer.
+tobytes(s)
+ Take a text string, a byte string, or a sequence of character taken from
+ a byte string and make a byte string.
+"""
+
+import sys
+import abc
+
+
+if sys.version_info[0] == 2:
+ def b(s):
+ return s
+ def bchr(s):
+ return chr(s)
+ def bstr(s):
+ return str(s)
+ def bord(s):
+ return ord(s)
+ def tobytes(s, encoding="latin-1"):
+ if isinstance(s, unicode):
+ return s.encode(encoding)
+ elif isinstance(s, str):
+ return s
+ elif isinstance(s, bytearray):
+ return bytes(s)
+ elif isinstance(s, memoryview):
+ return s.tobytes()
+ else:
+ return ''.join(s)
+ def tostr(bs):
+ return bs
+ def byte_string(s):
+ return isinstance(s, str)
+
+ # In Python 2, a memoryview does not support concatenation
+ def concat_buffers(a, b):
+ if isinstance(a, memoryview):
+ a = a.tobytes()
+ if isinstance(b, memoryview):
+ b = b.tobytes()
+ return a + b
+
+ from StringIO import StringIO
+ BytesIO = StringIO
+
+ from sys import maxint
+
+ iter_range = xrange
+
+ def is_native_int(x):
+ return isinstance(x, (int, long))
+
+ def is_string(x):
+ return isinstance(x, basestring)
+
+ def is_bytes(x):
+ return isinstance(x, str) or \
+ isinstance(x, bytearray) or \
+ isinstance(x, memoryview)
+
+ ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()})
+
+ FileNotFoundError = IOError
+
+else:
+ def b(s):
+ return s.encode("latin-1") # utf-8 would cause some side-effects we don't want
+ def bchr(s):
+ return bytes([s])
+ def bstr(s):
+ if isinstance(s,str):
+ return bytes(s,"latin-1")
+ else:
+ return bytes(s)
+ def bord(s):
+ return s
+ def tobytes(s, encoding="latin-1"):
+ if isinstance(s, bytes):
+ return s
+ elif isinstance(s, bytearray):
+ return bytes(s)
+ elif isinstance(s,str):
+ return s.encode(encoding)
+ elif isinstance(s, memoryview):
+ return s.tobytes()
+ else:
+ return bytes([s])
+ def tostr(bs):
+ return bs.decode("latin-1")
+ def byte_string(s):
+ return isinstance(s, bytes)
+
+ def concat_buffers(a, b):
+ return a + b
+
+ from io import BytesIO
+ from io import StringIO
+ from sys import maxsize as maxint
+
+ iter_range = range
+
+ def is_native_int(x):
+ return isinstance(x, int)
+
+ def is_string(x):
+ return isinstance(x, str)
+
+ def is_bytes(x):
+ return isinstance(x, bytes) or \
+ isinstance(x, bytearray) or \
+ isinstance(x, memoryview)
+
+ from abc import ABC
+
+ FileNotFoundError = FileNotFoundError
+
+
+def _copy_bytes(start, end, seq):
+ """Return an immutable copy of a sequence (byte string, byte array, memoryview)
+ in a certain interval [start:seq]"""
+
+ if isinstance(seq, memoryview):
+ return seq[start:end].tobytes()
+ elif isinstance(seq, bytearray):
+ return bytes(seq[start:end])
+ else:
+ return seq[start:end]
+
+del sys
+del abc
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/py3compat.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Util/py3compat.pyi
new file mode 100644
index 00000000..74e04a2b
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/py3compat.pyi
@@ -0,0 +1,33 @@
+from typing import Union, Any, Optional, IO
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+import sys
+
+def b(s: str) -> bytes: ...
+def bchr(s: int) -> bytes: ...
+def bord(s: bytes) -> int: ...
+def tobytes(s: Union[bytes, str]) -> bytes: ...
+def tostr(b: bytes) -> str: ...
+def bytestring(x: Any) -> bool: ...
+
+def is_native_int(s: Any) -> bool: ...
+def is_string(x: Any) -> bool: ...
+def is_bytes(x: Any) -> bool: ...
+
+def BytesIO(b: bytes) -> IO[bytes]: ...
+def StringIO(s: str) -> IO[str]: ...
+
+if sys.version_info[0] == 2:
+ from sys import maxint
+ iter_range = xrange
+
+else:
+ from sys import maxsize as maxint
+ iter_range = range
+
+class FileNotFoundError:
+ def __init__(self, err: int, msg: str, filename: str) -> None:
+ pass
+
+def _copy_bytes(start: Optional[int], end: Optional[int], seq: Buffer) -> bytes: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/strxor.py b/backend/venv/lib/python3.9/site-packages/Crypto/Util/strxor.py
new file mode 100644
index 00000000..362db6e6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/strxor.py
@@ -0,0 +1,146 @@
+# ===================================================================
+#
+# Copyright (c) 2014, Legrandin
+# 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.Util._raw_api import (load_pycryptodome_raw_lib, c_size_t,
+ create_string_buffer, get_raw_buffer,
+ c_uint8_ptr, is_writeable_buffer)
+
+_raw_strxor = load_pycryptodome_raw_lib(
+ "Crypto.Util._strxor",
+ """
+ void strxor(const uint8_t *in1,
+ const uint8_t *in2,
+ uint8_t *out, size_t len);
+ void strxor_c(const uint8_t *in,
+ uint8_t c,
+ uint8_t *out,
+ size_t len);
+ """)
+
+
+def strxor(term1, term2, output=None):
+ """From two byte strings of equal length,
+ create a third one which is the byte-by-byte XOR of the two.
+
+ Args:
+ term1 (bytes/bytearray/memoryview):
+ The first byte string to XOR.
+ term2 (bytes/bytearray/memoryview):
+ The second byte string to XOR.
+ output (bytearray/memoryview):
+ The location where the result will be written to.
+ It must have the same length as ``term1`` and ``term2``.
+ If ``None``, the result is returned.
+ :Return:
+ If ``output`` is ``None``, a new byte string with the result.
+ Otherwise ``None``.
+
+ .. note::
+ ``term1`` and ``term2`` must have the same length.
+ """
+
+ if len(term1) != len(term2):
+ raise ValueError("Only byte strings of equal length can be xored")
+
+ if output is None:
+ result = create_string_buffer(len(term1))
+ else:
+ # Note: output may overlap with either input
+ result = output
+
+ if not is_writeable_buffer(output):
+ raise TypeError("output must be a bytearray or a writeable memoryview")
+
+ if len(term1) != len(output):
+ raise ValueError("output must have the same length as the input"
+ " (%d bytes)" % len(term1))
+
+ _raw_strxor.strxor(c_uint8_ptr(term1),
+ c_uint8_ptr(term2),
+ c_uint8_ptr(result),
+ c_size_t(len(term1)))
+
+ if output is None:
+ return get_raw_buffer(result)
+ else:
+ return None
+
+
+def strxor_c(term, c, output=None):
+ """From a byte string, create a second one of equal length
+ where each byte is XOR-red with the same value.
+
+ Args:
+ term(bytes/bytearray/memoryview):
+ The byte string to XOR.
+ c (int):
+ Every byte in the string will be XOR-ed with this value.
+ It must be between 0 and 255 (included).
+ output (None or bytearray/memoryview):
+ The location where the result will be written to.
+ It must have the same length as ``term``.
+ If ``None``, the result is returned.
+
+ Return:
+ If ``output`` is ``None``, a new ``bytes`` string with the result.
+ Otherwise ``None``.
+ """
+
+ if not 0 <= c < 256:
+ raise ValueError("c must be in range(256)")
+
+ if output is None:
+ result = create_string_buffer(len(term))
+ else:
+ # Note: output may overlap with either input
+ result = output
+
+ if not is_writeable_buffer(output):
+ raise TypeError("output must be a bytearray or a writeable memoryview")
+
+ if len(term) != len(output):
+ raise ValueError("output must have the same length as the input"
+ " (%d bytes)" % len(term))
+
+ _raw_strxor.strxor_c(c_uint8_ptr(term),
+ c,
+ c_uint8_ptr(result),
+ c_size_t(len(term))
+ )
+
+ if output is None:
+ return get_raw_buffer(result)
+ else:
+ return None
+
+
+def _strxor_direct(term1, term2, result):
+ """Very fast XOR - check conditions!"""
+ _raw_strxor.strxor(term1, term2, result, c_size_t(len(term1)))
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/Util/strxor.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/Util/strxor.pyi
new file mode 100644
index 00000000..ca896f3b
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/Util/strxor.pyi
@@ -0,0 +1,6 @@
+from typing import Union, Optional
+
+Buffer = Union[bytes, bytearray, memoryview]
+
+def strxor(term1: bytes, term2: bytes, output: Optional[Buffer]=...) -> bytes: ...
+def strxor_c(term: bytes, c: int, output: Optional[Buffer]=...) -> bytes: ...
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/__init__.py b/backend/venv/lib/python3.9/site-packages/Crypto/__init__.py
new file mode 100644
index 00000000..1fc2db90
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/__init__.py
@@ -0,0 +1,6 @@
+__all__ = ['Cipher', 'Hash', 'Protocol', 'PublicKey', 'Util', 'Signature',
+ 'IO', 'Math']
+
+version_info = (3, 23, '0')
+
+__version__ = ".".join([str(x) for x in version_info])
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/__init__.pyi b/backend/venv/lib/python3.9/site-packages/Crypto/__init__.pyi
new file mode 100644
index 00000000..bc73446d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/Crypto/__init__.pyi
@@ -0,0 +1,4 @@
+from typing import Tuple, Union
+
+version_info : Tuple[int, int, Union[int, str]]
+__version__ : str
diff --git a/backend/venv/lib/python3.9/site-packages/Crypto/py.typed b/backend/venv/lib/python3.9/site-packages/Crypto/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/INSTALLER b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/INSTALLER
new file mode 100644
index 00000000..a1b589e3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/LICENSE b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/LICENSE
new file mode 100644
index 00000000..86b18e10
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2010, 2013 PyMySQL contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/METADATA b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/METADATA
new file mode 100644
index 00000000..5477d25f
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/METADATA
@@ -0,0 +1,134 @@
+Metadata-Version: 2.1
+Name: PyMySQL
+Version: 1.1.0
+Summary: Pure Python MySQL Driver
+Author-email: Inada Naoki , Yutaka Matsubara
+License: MIT License
+Project-URL: Project, https://github.com/PyMySQL/PyMySQL
+Project-URL: Documentation, https://pymysql.readthedocs.io/
+Keywords: MySQL
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Topic :: Database
+Requires-Python: >=3.7
+Description-Content-Type: text/markdown
+License-File: LICENSE
+Provides-Extra: ed25519
+Requires-Dist: PyNaCl (>=1.4.0) ; extra == 'ed25519'
+Provides-Extra: rsa
+Requires-Dist: cryptography ; extra == 'rsa'
+
+[](https://pymysql.readthedocs.io/)
+[](https://codecov.io/gh/PyMySQL/PyMySQL)
+
+# PyMySQL
+
+This package contains a pure-Python MySQL client library, based on [PEP
+249](https://www.python.org/dev/peps/pep-0249/).
+
+## Requirements
+
+- Python -- one of the following:
+ - [CPython](https://www.python.org/) : 3.7 and newer
+ - [PyPy](https://pypy.org/) : Latest 3.x version
+- MySQL Server -- one of the following:
+ - [MySQL](https://www.mysql.com/) \>= 5.7
+ - [MariaDB](https://mariadb.org/) \>= 10.4
+
+## Installation
+
+Package is uploaded on [PyPI](https://pypi.org/project/PyMySQL).
+
+You can install it with pip:
+
+ $ python3 -m pip install PyMySQL
+
+To use "sha256_password" or "caching_sha2_password" for authenticate,
+you need to install additional dependency:
+
+ $ python3 -m pip install PyMySQL[rsa]
+
+To use MariaDB's "ed25519" authentication method, you need to install
+additional dependency:
+
+ $ python3 -m pip install PyMySQL[ed25519]
+
+## Documentation
+
+Documentation is available online:
+
+For support, please refer to the
+[StackOverflow](https://stackoverflow.com/questions/tagged/pymysql).
+
+## Example
+
+The following examples make use of a simple table
+
+``` sql
+CREATE TABLE `users` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `email` varchar(255) COLLATE utf8_bin NOT NULL,
+ `password` varchar(255) COLLATE utf8_bin NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
+AUTO_INCREMENT=1 ;
+```
+
+``` python
+import pymysql.cursors
+
+# Connect to the database
+connection = pymysql.connect(host='localhost',
+ user='user',
+ password='passwd',
+ database='db',
+ cursorclass=pymysql.cursors.DictCursor)
+
+with connection:
+ with connection.cursor() as cursor:
+ # Create a new record
+ sql = "INSERT INTO `users` (`email`, `password`) VALUES (%s, %s)"
+ cursor.execute(sql, ('webmaster@python.org', 'very-secret'))
+
+ # connection is not autocommit by default. So you must commit to save
+ # your changes.
+ connection.commit()
+
+ with connection.cursor() as cursor:
+ # Read a single record
+ sql = "SELECT `id`, `password` FROM `users` WHERE `email`=%s"
+ cursor.execute(sql, ('webmaster@python.org',))
+ result = cursor.fetchone()
+ print(result)
+```
+
+This example will print:
+
+``` python
+{'password': 'very-secret', 'id': 1}
+```
+
+## Resources
+
+- DB-API 2.0:
+- MySQL Reference Manuals:
+- MySQL client/server protocol:
+
+- "Connector" channel in MySQL Community Slack:
+
+- PyMySQL mailing list:
+
+
+## License
+
+PyMySQL is released under the MIT License. See LICENSE for more
+information.
diff --git a/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/RECORD b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/RECORD
new file mode 100644
index 00000000..3a09f4d3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/RECORD
@@ -0,0 +1,43 @@
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/_auth.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/charset.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/connections.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/constants/CLIENT.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/constants/COMMAND.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/constants/CR.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/constants/ER.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/constants/FIELD_TYPE.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/constants/FLAG.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/constants/SERVER_STATUS.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/constants/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/converters.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/cursors.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/err.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/optionfile.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/protocol.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/pymysql/times.cpython-39.pyc,,
+PyMySQL-1.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+PyMySQL-1.1.0.dist-info/LICENSE,sha256=MUEg3GXwgA9ziksxQAx27hTezR--d86cNUCkIbhup7Y,1070
+PyMySQL-1.1.0.dist-info/METADATA,sha256=FIAoGrL3L7e8pvWz1KxL5Wx7CtXH_QOwtTHq_hjBjYQ,4355
+PyMySQL-1.1.0.dist-info/RECORD,,
+PyMySQL-1.1.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+PyMySQL-1.1.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
+PyMySQL-1.1.0.dist-info/top_level.txt,sha256=IKlV-f4o90sOdnMd6HBvo0l2nqfJOGUzkwZeaEEGuRg,8
+pymysql/__init__.py,sha256=j699mDBexrjMZyGsM6LTZeww5aLtJfcAEpXJyJc6zac,4264
+pymysql/_auth.py,sha256=vDQm9OjORdkofdXiQMQ49RLWypMxa5zKLoS_GnvIcyQ,7416
+pymysql/charset.py,sha256=_f1uIga7AaWoeKLXzA-9Xra9jYPqqgDiT78ikqtn5yE,10238
+pymysql/connections.py,sha256=nktipI748AaKRu6q6hv0CsZ3KG6K9tWAkAWKsbwgSEg,53589
+pymysql/constants/CLIENT.py,sha256=SSvMFPZCTVMU1UWa4zOrfhYMDdR2wG2mS0E5GzJhDsg,878
+pymysql/constants/COMMAND.py,sha256=TGITAUcNWlq2Gwg2wv5UK2ykdTd4LYTk_EcJJOCpGIc,679
+pymysql/constants/CR.py,sha256=Qk35FWRMxRHd6Sa9CCIATMh7jegR3xnLdrdaBCT0dTQ,2320
+pymysql/constants/ER.py,sha256=nwqX_r0o4mmN4Cxm7NVRyJOTVov_5Gbl5peGe6oz5fk,12357
+pymysql/constants/FIELD_TYPE.py,sha256=ytFzgAnGmb9hvdsBlnK68qdZv_a6jYFIXT6VSAb60z8,370
+pymysql/constants/FLAG.py,sha256=Fy-PrCLnUI7fx_o5WypYnUAzWAM0E9d5yL8fFRVKffY,214
+pymysql/constants/SERVER_STATUS.py,sha256=m28Iq5JGCFCWLhafE73-iOvw_9gDGqnytW3NkHpbugA,333
+pymysql/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+pymysql/converters.py,sha256=wxPYTl9matiMD-KYKtjB5ujHWllj1jc-kwWM6-L0oms,9591
+pymysql/cursors.py,sha256=a4-JHYP148kx-9qVNRz8vTtlilGlKDbk_QtFlWph5L4,16535
+pymysql/err.py,sha256=bpxayM4IUnFQAd8bUZ3PFsFomi9QSfBk-0TJXyKU2FI,3773
+pymysql/optionfile.py,sha256=eQoz6c43yvmHtp5MI9TB2GPRdoggOLemcUWABksfutk,651
+pymysql/protocol.py,sha256=zcYHCurGOymDgNo1DcCKThi_8zUnQOgaiu3M2VpqzfM,11863
+pymysql/times.py,sha256=_qXgDaYwsHntvpIKSKXp1rrYIgtq6Z9pLyLnO2XNoL0,360
diff --git a/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/REQUESTED b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/REQUESTED
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/WHEEL b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/WHEEL
new file mode 100644
index 00000000..1f37c02f
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.40.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/top_level.txt b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/top_level.txt
new file mode 100644
index 00000000..d4a7eda5
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/PyMySQL-1.1.0.dist-info/top_level.txt
@@ -0,0 +1 @@
+pymysql
diff --git a/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/INSTALLER b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/INSTALLER
new file mode 100644
index 00000000..a1b589e3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/LICENSE b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/LICENSE
new file mode 100644
index 00000000..967cdc5d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2005-2024 SQLAlchemy authors and contributors .
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/METADATA b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/METADATA
new file mode 100644
index 00000000..ac36a34c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/METADATA
@@ -0,0 +1,242 @@
+Metadata-Version: 2.1
+Name: SQLAlchemy
+Version: 2.0.25
+Summary: Database Abstraction Library
+Home-page: https://www.sqlalchemy.org
+Author: Mike Bayer
+Author-email: mike_mp@zzzcomputing.com
+License: MIT
+Project-URL: Documentation, https://docs.sqlalchemy.org
+Project-URL: Issue Tracker, https://github.com/sqlalchemy/sqlalchemy/
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Database :: Front-Ends
+Requires-Python: >=3.7
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+Requires-Dist: typing-extensions >=4.6.0
+Requires-Dist: greenlet !=0.4.17 ; platform_machine == "aarch64" or (platform_machine == "ppc64le" or (platform_machine == "x86_64" or (platform_machine == "amd64" or (platform_machine == "AMD64" or (platform_machine == "win32" or platform_machine == "WIN32")))))
+Requires-Dist: importlib-metadata ; python_version < "3.8"
+Provides-Extra: aiomysql
+Requires-Dist: greenlet !=0.4.17 ; extra == 'aiomysql'
+Requires-Dist: aiomysql >=0.2.0 ; extra == 'aiomysql'
+Provides-Extra: aioodbc
+Requires-Dist: greenlet !=0.4.17 ; extra == 'aioodbc'
+Requires-Dist: aioodbc ; extra == 'aioodbc'
+Provides-Extra: aiosqlite
+Requires-Dist: greenlet !=0.4.17 ; extra == 'aiosqlite'
+Requires-Dist: aiosqlite ; extra == 'aiosqlite'
+Requires-Dist: typing-extensions !=3.10.0.1 ; extra == 'aiosqlite'
+Provides-Extra: asyncio
+Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncio'
+Provides-Extra: asyncmy
+Requires-Dist: greenlet !=0.4.17 ; extra == 'asyncmy'
+Requires-Dist: asyncmy !=0.2.4,!=0.2.6,>=0.2.3 ; extra == 'asyncmy'
+Provides-Extra: mariadb_connector
+Requires-Dist: mariadb !=1.1.2,!=1.1.5,>=1.0.1 ; extra == 'mariadb_connector'
+Provides-Extra: mssql
+Requires-Dist: pyodbc ; extra == 'mssql'
+Provides-Extra: mssql_pymssql
+Requires-Dist: pymssql ; extra == 'mssql_pymssql'
+Provides-Extra: mssql_pyodbc
+Requires-Dist: pyodbc ; extra == 'mssql_pyodbc'
+Provides-Extra: mypy
+Requires-Dist: mypy >=0.910 ; extra == 'mypy'
+Provides-Extra: mysql
+Requires-Dist: mysqlclient >=1.4.0 ; extra == 'mysql'
+Provides-Extra: mysql_connector
+Requires-Dist: mysql-connector-python ; extra == 'mysql_connector'
+Provides-Extra: oracle
+Requires-Dist: cx-oracle >=8 ; extra == 'oracle'
+Provides-Extra: oracle_oracledb
+Requires-Dist: oracledb >=1.0.1 ; extra == 'oracle_oracledb'
+Provides-Extra: postgresql
+Requires-Dist: psycopg2 >=2.7 ; extra == 'postgresql'
+Provides-Extra: postgresql_asyncpg
+Requires-Dist: greenlet !=0.4.17 ; extra == 'postgresql_asyncpg'
+Requires-Dist: asyncpg ; extra == 'postgresql_asyncpg'
+Provides-Extra: postgresql_pg8000
+Requires-Dist: pg8000 >=1.29.1 ; extra == 'postgresql_pg8000'
+Provides-Extra: postgresql_psycopg
+Requires-Dist: psycopg >=3.0.7 ; extra == 'postgresql_psycopg'
+Provides-Extra: postgresql_psycopg2binary
+Requires-Dist: psycopg2-binary ; extra == 'postgresql_psycopg2binary'
+Provides-Extra: postgresql_psycopg2cffi
+Requires-Dist: psycopg2cffi ; extra == 'postgresql_psycopg2cffi'
+Provides-Extra: postgresql_psycopgbinary
+Requires-Dist: psycopg[binary] >=3.0.7 ; extra == 'postgresql_psycopgbinary'
+Provides-Extra: pymysql
+Requires-Dist: pymysql ; extra == 'pymysql'
+Provides-Extra: sqlcipher
+Requires-Dist: sqlcipher3-binary ; extra == 'sqlcipher'
+
+SQLAlchemy
+==========
+
+|PyPI| |Python| |Downloads|
+
+.. |PyPI| image:: https://img.shields.io/pypi/v/sqlalchemy
+ :target: https://pypi.org/project/sqlalchemy
+ :alt: PyPI
+
+.. |Python| image:: https://img.shields.io/pypi/pyversions/sqlalchemy
+ :target: https://pypi.org/project/sqlalchemy
+ :alt: PyPI - Python Version
+
+.. |Downloads| image:: https://static.pepy.tech/badge/sqlalchemy/month
+ :target: https://pepy.tech/project/sqlalchemy
+ :alt: PyPI - Downloads
+
+
+The Python SQL Toolkit and Object Relational Mapper
+
+Introduction
+-------------
+
+SQLAlchemy is the Python SQL toolkit and Object Relational Mapper
+that gives application developers the full power and
+flexibility of SQL. SQLAlchemy provides a full suite
+of well known enterprise-level persistence patterns,
+designed for efficient and high-performing database
+access, adapted into a simple and Pythonic domain
+language.
+
+Major SQLAlchemy features include:
+
+* An industrial strength ORM, built
+ from the core on the identity map, unit of work,
+ and data mapper patterns. These patterns
+ allow transparent persistence of objects
+ using a declarative configuration system.
+ Domain models
+ can be constructed and manipulated naturally,
+ and changes are synchronized with the
+ current transaction automatically.
+* A relationally-oriented query system, exposing
+ the full range of SQL's capabilities
+ explicitly, including joins, subqueries,
+ correlation, and most everything else,
+ in terms of the object model.
+ Writing queries with the ORM uses the same
+ techniques of relational composition you use
+ when writing SQL. While you can drop into
+ literal SQL at any time, it's virtually never
+ needed.
+* A comprehensive and flexible system
+ of eager loading for related collections and objects.
+ Collections are cached within a session,
+ and can be loaded on individual access, all
+ at once using joins, or by query per collection
+ across the full result set.
+* A Core SQL construction system and DBAPI
+ interaction layer. The SQLAlchemy Core is
+ separate from the ORM and is a full database
+ abstraction layer in its own right, and includes
+ an extensible Python-based SQL expression
+ language, schema metadata, connection pooling,
+ type coercion, and custom types.
+* All primary and foreign key constraints are
+ assumed to be composite and natural. Surrogate
+ integer primary keys are of course still the
+ norm, but SQLAlchemy never assumes or hardcodes
+ to this model.
+* Database introspection and generation. Database
+ schemas can be "reflected" in one step into
+ Python structures representing database metadata;
+ those same structures can then generate
+ CREATE statements right back out - all within
+ the Core, independent of the ORM.
+
+SQLAlchemy's philosophy:
+
+* SQL databases behave less and less like object
+ collections the more size and performance start to
+ matter; object collections behave less and less like
+ tables and rows the more abstraction starts to matter.
+ SQLAlchemy aims to accommodate both of these
+ principles.
+* An ORM doesn't need to hide the "R". A relational
+ database provides rich, set-based functionality
+ that should be fully exposed. SQLAlchemy's
+ ORM provides an open-ended set of patterns
+ that allow a developer to construct a custom
+ mediation layer between a domain model and
+ a relational schema, turning the so-called
+ "object relational impedance" issue into
+ a distant memory.
+* The developer, in all cases, makes all decisions
+ regarding the design, structure, and naming conventions
+ of both the object model as well as the relational
+ schema. SQLAlchemy only provides the means
+ to automate the execution of these decisions.
+* With SQLAlchemy, there's no such thing as
+ "the ORM generated a bad query" - you
+ retain full control over the structure of
+ queries, including how joins are organized,
+ how subqueries and correlation is used, what
+ columns are requested. Everything SQLAlchemy
+ does is ultimately the result of a developer-initiated
+ decision.
+* Don't use an ORM if the problem doesn't need one.
+ SQLAlchemy consists of a Core and separate ORM
+ component. The Core offers a full SQL expression
+ language that allows Pythonic construction
+ of SQL constructs that render directly to SQL
+ strings for a target database, returning
+ result sets that are essentially enhanced DBAPI
+ cursors.
+* Transactions should be the norm. With SQLAlchemy's
+ ORM, nothing goes to permanent storage until
+ commit() is called. SQLAlchemy encourages applications
+ to create a consistent means of delineating
+ the start and end of a series of operations.
+* Never render a literal value in a SQL statement.
+ Bound parameters are used to the greatest degree
+ possible, allowing query optimizers to cache
+ query plans effectively and making SQL injection
+ attacks a non-issue.
+
+Documentation
+-------------
+
+Latest documentation is at:
+
+https://www.sqlalchemy.org/docs/
+
+Installation / Requirements
+---------------------------
+
+Full documentation for installation is at
+`Installation `_.
+
+Getting Help / Development / Bug reporting
+------------------------------------------
+
+Please refer to the `SQLAlchemy Community Guide `_.
+
+Code of Conduct
+---------------
+
+Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
+constructive communication between users and developers.
+Please see our current Code of Conduct at
+`Code of Conduct `_.
+
+License
+-------
+
+SQLAlchemy is distributed under the `MIT license
+`_.
+
diff --git a/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/RECORD b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/RECORD
new file mode 100644
index 00000000..8c7e1ab7
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/RECORD
@@ -0,0 +1,530 @@
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/connectors/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/connectors/aioodbc.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/connectors/asyncio.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/connectors/pyodbc.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/cyextension/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/_typing.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mssql/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mssql/aioodbc.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mssql/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mssql/information_schema.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mssql/json.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mssql/provision.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mssql/pymssql.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mssql/pyodbc.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/aiomysql.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/asyncmy.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/cymysql.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/dml.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/enumerated.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/expression.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/json.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/mariadb.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/mariadbconnector.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/mysqlconnector.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/mysqldb.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/provision.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/pymysql.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/pyodbc.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/reflection.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/reserved_words.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/mysql/types.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/oracle/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/oracle/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/oracle/cx_oracle.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/oracle/dictionary.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/oracle/oracledb.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/oracle/provision.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/oracle/types.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/_psycopg_common.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/array.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/asyncpg.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/dml.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/ext.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/hstore.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/json.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/named_types.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/operators.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/pg8000.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/pg_catalog.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/provision.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/psycopg.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/psycopg2.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/psycopg2cffi.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/ranges.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/postgresql/types.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/sqlite/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/sqlite/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/sqlite/dml.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/sqlite/json.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/sqlite/provision.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/sqlite/pysqlcipher.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/dialects/sqlite/pysqlite.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/_py_processors.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/_py_row.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/_py_util.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/characteristics.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/create.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/cursor.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/default.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/events.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/interfaces.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/mock.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/processors.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/reflection.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/result.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/row.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/strategies.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/url.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/engine/util.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/event/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/event/api.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/event/attr.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/event/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/event/legacy.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/event/registry.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/events.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/exc.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/associationproxy.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/asyncio/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/asyncio/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/asyncio/engine.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/asyncio/exc.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/asyncio/result.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/asyncio/scoping.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/asyncio/session.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/automap.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/baked.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/compiler.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/declarative/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/declarative/extensions.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/horizontal_shard.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/hybrid.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/indexable.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/instrumentation.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/mutable.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/mypy/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/mypy/apply.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/mypy/decl_class.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/mypy/infer.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/mypy/names.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/mypy/plugin.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/mypy/util.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/orderinglist.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/ext/serializer.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/future/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/future/engine.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/inspection.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/log.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/_orm_constructors.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/_typing.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/attributes.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/bulk_persistence.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/clsregistry.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/collections.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/context.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/decl_api.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/decl_base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/dependency.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/descriptor_props.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/dynamic.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/evaluator.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/events.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/exc.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/identity.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/instrumentation.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/interfaces.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/loading.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/mapped_collection.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/mapper.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/path_registry.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/persistence.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/properties.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/query.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/relationships.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/scoping.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/session.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/state.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/state_changes.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/strategies.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/strategy_options.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/sync.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/unitofwork.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/util.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/orm/writeonly.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/pool/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/pool/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/pool/events.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/pool/impl.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/schema.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/_dml_constructors.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/_elements_constructors.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/_orm_types.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/_py_util.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/_selectable_constructors.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/_typing.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/annotation.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/cache_key.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/coercions.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/compiler.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/crud.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/ddl.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/default_comparator.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/dml.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/elements.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/events.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/expression.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/functions.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/lambdas.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/naming.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/operators.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/roles.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/schema.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/selectable.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/sqltypes.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/traversals.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/type_api.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/util.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/sql/visitors.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/assertions.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/assertsql.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/asyncio.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/config.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/engines.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/entities.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/exclusions.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/fixtures/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/fixtures/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/fixtures/mypy.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/fixtures/orm.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/fixtures/sql.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/pickleable.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/plugin/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/plugin/bootstrap.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/plugin/plugin_base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/plugin/pytestplugin.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/profiling.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/provision.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/requirements.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/schema.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_cte.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_ddl.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_deprecations.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_dialect.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_insert.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_reflection.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_results.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_rowcount.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_select.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_sequence.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_types.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_unicode_ddl.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/suite/test_update_delete.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/util.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/testing/warnings.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/types.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/_collections.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/_concurrency_py3k.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/_has_cy.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/_py_collections.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/compat.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/concurrency.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/deprecations.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/langhelpers.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/preloaded.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/queue.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/tool_support.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/topological.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/sqlalchemy/util/typing.cpython-39.pyc,,
+SQLAlchemy-2.0.25.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+SQLAlchemy-2.0.25.dist-info/LICENSE,sha256=PA9Zq4h9BB3mpOUv_j6e212VIt6Qn66abNettue-MpM,1100
+SQLAlchemy-2.0.25.dist-info/METADATA,sha256=e57J_l66lNZ5LyXmLMbAGiL02rqx7HxQu_Ci5m_5Y8U,9602
+SQLAlchemy-2.0.25.dist-info/RECORD,,
+SQLAlchemy-2.0.25.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+SQLAlchemy-2.0.25.dist-info/WHEEL,sha256=JlTVwpqUzP01iqSUKAL8LrwscoYFkCFkZ28SRV9G62c,108
+SQLAlchemy-2.0.25.dist-info/top_level.txt,sha256=rp-ZgB7D8G11ivXON5VGPjupT1voYmWqkciDt5Uaw_Q,11
+sqlalchemy/__init__.py,sha256=Tu8hhzZF610d9j59ruCV2IROKp-u1Y9i-Plhc6Nf50c,13033
+sqlalchemy/connectors/__init__.py,sha256=PzXPqZqi3BzEnrs1eW0DcsR4lyknAzhhN9rWcQ97hb4,476
+sqlalchemy/connectors/aioodbc.py,sha256=GSTiNMO9h0qjPxgqaxDwWZ8HvhWMFNVR6MJQnN1oc40,5288
+sqlalchemy/connectors/asyncio.py,sha256=6s4hDYfuMjJ9KbJ4s7bF1fp5DmcgV77ozgZ5-bwZ0wc,5955
+sqlalchemy/connectors/pyodbc.py,sha256=PZC86t3poFmhgW9_tjDJH8o1Ua0OyiCdfrP7GRX5Gxc,8453
+sqlalchemy/cyextension/__init__.py,sha256=GzhhN8cjMnDTE0qerlUlpbrNmFPHQWCZ4Gk74OAxl04,244
+sqlalchemy/cyextension/collections.cpython-39-darwin.so,sha256=XO5_3Ml86-Y0cfYbsfw_FNyxVWI88OMul7FUyXA4Lyw,256573
+sqlalchemy/cyextension/collections.pyx,sha256=L7DZ3DGKpgw2MT2ZZRRxCnrcyE5pU1NAFowWgAzQPEc,12571
+sqlalchemy/cyextension/immutabledict.cpython-39-darwin.so,sha256=BUGQhF2CR4DZdWxDYbVo6hRu6Xe04FGNulIABNamzJw,125583
+sqlalchemy/cyextension/immutabledict.pxd,sha256=3x3-rXG5eRQ7bBnktZ-OJ9-6ft8zToPmTDOd92iXpB0,291
+sqlalchemy/cyextension/immutabledict.pyx,sha256=KfDTYbTfebstE8xuqAtuXsHNAK0_b5q_ymUiinUe_xs,3535
+sqlalchemy/cyextension/processors.cpython-39-darwin.so,sha256=S447QKxcbBYfRWf7XM1VXBa4Q5qw68sSiEy6_kw7BR4,105052
+sqlalchemy/cyextension/processors.pyx,sha256=R1rHsGLEaGeBq5VeCydjClzYlivERIJ9B-XLOJlf2MQ,1792
+sqlalchemy/cyextension/resultproxy.cpython-39-darwin.so,sha256=Z60naxDMlmr_-WsUs3546krkTGYaPAIUvJFCJZBjDSY,108013
+sqlalchemy/cyextension/resultproxy.pyx,sha256=eWLdyBXiBy_CLQrF5ScfWJm7X0NeelscSXedtj1zv9Q,2725
+sqlalchemy/cyextension/util.cpython-39-darwin.so,sha256=EqW2gwRAgXQihLhKx01et1sg5H2BOiBV8CldS3iROzE,125462
+sqlalchemy/cyextension/util.pyx,sha256=B85orxa9LddLuQEaDoVSq1XmAXIbLKxrxpvuB8ogV_o,2530
+sqlalchemy/dialects/__init__.py,sha256=Kos9Gf5JZg1Vg6GWaCqEbD6e0r1jCwCmcnJIfcxDdcY,1770
+sqlalchemy/dialects/_typing.py,sha256=hyv0nKucX2gI8ispB1IsvaUgrEPn9zEcq9hS7kfstEw,888
+sqlalchemy/dialects/mssql/__init__.py,sha256=r5t8wFRNtBQoiUWh0WfIEWzXZW6f3D0uDt6NZTW_7Cc,1880
+sqlalchemy/dialects/mssql/aioodbc.py,sha256=UQd9ecSMIML713TDnLAviuBVJle7P7i1FtqGZZePk2Y,2022
+sqlalchemy/dialects/mssql/base.py,sha256=lkOGhA8Kg3aximjmbAOZcShXlSWLZWEe94-KdfzxkMo,133650
+sqlalchemy/dialects/mssql/information_schema.py,sha256=ZmFLZ7d4qlguBTm5pIAe3XfnCOr8qZfXPDFK5DE7in8,8083
+sqlalchemy/dialects/mssql/json.py,sha256=evUACW2O62TAPq8B7QIPagz7jfc664ql9ms68JqiYzg,4816
+sqlalchemy/dialects/mssql/provision.py,sha256=RTVbgYLFAHzEnpVQDJroU8ji_10MqBTiZfyP9_-QNT4,5362
+sqlalchemy/dialects/mssql/pymssql.py,sha256=eZRLz7HGt3SdoZUjFBmA9BS43N7AhIASw7VPBPEJuG0,4038
+sqlalchemy/dialects/mssql/pyodbc.py,sha256=GqWKptZfVMKZJ7RXQyuXL4pRJlfnOrZtRYchEeeHl3o,27057
+sqlalchemy/dialects/mysql/__init__.py,sha256=bxbi4hkysUK2OOVvr1F49akUj1cky27kKb07tgFzI9U,2153
+sqlalchemy/dialects/mysql/aiomysql.py,sha256=tGe7R8lfaRhdRNMJHQg-AocWu5UTkstLg1C_yIKFBl8,9759
+sqlalchemy/dialects/mysql/asyncmy.py,sha256=sAz0ctNETtEc_8vl03xroLTsEWS6CLNKcqjesm4Ne6Q,9828
+sqlalchemy/dialects/mysql/base.py,sha256=Zy_ZCzuMUeeFmaOPSjmVDP5oxBEw9898QTuxppQCoq8,120698
+sqlalchemy/dialects/mysql/cymysql.py,sha256=eXT1ry0w_qRxjiO24M980c-8PZ9qSsbhqBHntjEiKB0,2300
+sqlalchemy/dialects/mysql/dml.py,sha256=HXJMAvimJsqvhj3UZO4vW_6LkF5RqaKbHvklAjor7yU,7645
+sqlalchemy/dialects/mysql/enumerated.py,sha256=ipEPPQqoXfFwcywNdcLlZCEzHBtnitHRah1Gn6nItcg,8448
+sqlalchemy/dialects/mysql/expression.py,sha256=lsmQCHKwfPezUnt27d2kR6ohk4IRFCA64KBS16kx5dc,4097
+sqlalchemy/dialects/mysql/json.py,sha256=l6MEZ0qp8FgiRrIQvOMhyEJq0q6OqiEnvDTx5Cbt9uQ,2269
+sqlalchemy/dialects/mysql/mariadb.py,sha256=kTfBLioLKk4JFFst4TY_iWqPtnvvQXFHknLfm89H2N8,853
+sqlalchemy/dialects/mysql/mariadbconnector.py,sha256=VVRwKLb6GzDmitOM4wLNvmZw6RdhnIwkLl7IZfAmUy8,8734
+sqlalchemy/dialects/mysql/mysqlconnector.py,sha256=qiQdfLPze3QHuASAZ9iqRzD0hDW8FbKoQnfAEQCF7tM,5675
+sqlalchemy/dialects/mysql/mysqldb.py,sha256=9x_JiY4hj4tykG1ckuEGPyH4jCtsh4fgBhNukVnjUos,9658
+sqlalchemy/dialects/mysql/provision.py,sha256=4oGkClQ8jC3YLPF54sB4kCjFc8HRTwf5zl5zftAAXGo,3474
+sqlalchemy/dialects/mysql/pymysql.py,sha256=GUnSHd2M2uKjmN46Hheymtm26g7phEgwYOXrX0zLY8M,4083
+sqlalchemy/dialects/mysql/pyodbc.py,sha256=072crI4qVyPhajYvHnsfFeSrNjLFVPIjBQKo5uyz5yk,4297
+sqlalchemy/dialects/mysql/reflection.py,sha256=TsRocAsRbAisEgu5NWSND7DZg9OS9ZqwHyaxLzlEgnU,22565
+sqlalchemy/dialects/mysql/reserved_words.py,sha256=Dm7FINIAkrKLoXmdu26SpE6V8LDCGyp734nmHV2tMd0,9154
+sqlalchemy/dialects/mysql/types.py,sha256=aPzx7hqqZ21aGwByEC-yWZUl6OpMvkbxwTqdN3OUGGI,24267
+sqlalchemy/dialects/oracle/__init__.py,sha256=p4-2gw7TT0bX_MoJXTGD4i8WHctYsK9kCRbkpzykBrc,1493
+sqlalchemy/dialects/oracle/base.py,sha256=Ng_T-Xl1rOHJO7mcnTbINnTVEQUjeLIKBl1DutaBRdY,118045
+sqlalchemy/dialects/oracle/cx_oracle.py,sha256=21xgnOZ8kCKrIfC9XlZM7R2zKZa86nsBHx4cv0Iw3x8,55290
+sqlalchemy/dialects/oracle/dictionary.py,sha256=7WMrbPkqo8ZdGjaEZyQr-5f2pajSOF1OTGb8P97z8-g,19519
+sqlalchemy/dialects/oracle/oracledb.py,sha256=vDKUuy4DExEMnXeidZL0wjuggJAqTkhXtSvxkvaEjbs,9485
+sqlalchemy/dialects/oracle/provision.py,sha256=O9ZpF4OG6Cx4mMzLRfZwhs8dZjrJETWR402n9c7726A,8304
+sqlalchemy/dialects/oracle/types.py,sha256=QK3hJvWzKnnCe3oD3rItwEEIwcoBze8qGg7VFOvVlIk,8231
+sqlalchemy/dialects/postgresql/__init__.py,sha256=rlGgUbemHlQWvsDjqWufCU-CioMPtSHRDgCd8G0v10E,3743
+sqlalchemy/dialects/postgresql/_psycopg_common.py,sha256=7TudtgsPiSB8O5kX8W8KxcNYR8t5h_UHb86b_ChL0P8,5696
+sqlalchemy/dialects/postgresql/array.py,sha256=3EWWhFJbw2xJfie1RAqtscecCIXSGZM4qmOipLYc1T0,13691
+sqlalchemy/dialects/postgresql/asyncpg.py,sha256=KGBdzxbHnqFpWOe6usS5yxuw2KK_KBqSpp6RA7F0Ua8,40232
+sqlalchemy/dialects/postgresql/base.py,sha256=SVALrrOKYJfrlelcsUM50neHQzVAWcwA6RxQG84YM24,175627
+sqlalchemy/dialects/postgresql/dml.py,sha256=L3G3bVL41DXirham5XRBXYOM4eefhqyGCzMyn8zCdI0,11212
+sqlalchemy/dialects/postgresql/ext.py,sha256=1bZ--iNh2O9ym7l2gXZX48yP3yMO4dqb9RpYro2Mj2Q,16262
+sqlalchemy/dialects/postgresql/hstore.py,sha256=otAx-RTDfpi_tcXkMuQV0JOIXtYgevgnsikLKKOkI6U,11541
+sqlalchemy/dialects/postgresql/json.py,sha256=-ffnp85fQBOyt0Bjb7XAupmOxloUdzFZZgixUG3Wj5w,11212
+sqlalchemy/dialects/postgresql/named_types.py,sha256=i2GwHI8V83AA2Gr87yyVCNBsm9mF99as54UlyOBs7IY,17101
+sqlalchemy/dialects/postgresql/operators.py,sha256=NsAaWun_tL3d_be0fs9YL6T4LPKK6crnmFxxIJHgyeY,2808
+sqlalchemy/dialects/postgresql/pg8000.py,sha256=qnMSG3brW6XiygF-vXr1JguSSNpd-xlHJRkHHCDWs-k,18637
+sqlalchemy/dialects/postgresql/pg_catalog.py,sha256=nAKavWTE_4cqxiDKDTdo-ivkCxxRIlzD5GO9Wl1yrG4,8884
+sqlalchemy/dialects/postgresql/provision.py,sha256=yqyx-aDFO9l2YcL9f4T5HBP_Lnt5dHsMjpuXUG8mi7A,5762
+sqlalchemy/dialects/postgresql/psycopg.py,sha256=zOM1PfiGU1I-XnkF5N6pBEDvTLP0qZZg6YSWWNgd-Xw,22320
+sqlalchemy/dialects/postgresql/psycopg2.py,sha256=f6vuQ4BStwcdLRV2iVYwyqjksYSV-c_uJvCWawU6cfg,31601
+sqlalchemy/dialects/postgresql/psycopg2cffi.py,sha256=M7wAYSL6Pvt-4nbfacAHGyyw4XMKJ_bQZ1tc1pBtIdg,1756
+sqlalchemy/dialects/postgresql/ranges.py,sha256=mPsXfEz3Ot0QebOqp5dt9mmy_SATwdDQ7wVe-Q5Bqqc,30252
+sqlalchemy/dialects/postgresql/types.py,sha256=yoBV_hSq6m93mtXmWPX8LoOORNqWyLC1zyckgu656BI,7323
+sqlalchemy/dialects/sqlite/__init__.py,sha256=lp9DIggNn349M-7IYhUA8et8--e8FRExWD2V_r1LJk4,1182
+sqlalchemy/dialects/sqlite/aiosqlite.py,sha256=OMvxP2eWyqk5beF-sHhzxRmjzO4VCQp55q7NH2XPVTE,12305
+sqlalchemy/dialects/sqlite/base.py,sha256=irgfivk--Pf7nj2tIZHALGOWjeK4CDnTtvRg9vN0QjY,96794
+sqlalchemy/dialects/sqlite/dml.py,sha256=ZZ6RiyflrhtPwrgNQSYUCdUWobDnuXPN9yop0gJTm9c,8443
+sqlalchemy/dialects/sqlite/json.py,sha256=Eoplbb_4dYlfrtmQaI8Xddd2suAIHA-IdbDQYM-LIhs,2777
+sqlalchemy/dialects/sqlite/provision.py,sha256=UCpmwxf4IWlrpb2eLHGbPTpCFVbdI_KAh2mKtjiLYao,5632
+sqlalchemy/dialects/sqlite/pysqlcipher.py,sha256=OL2S_05DK9kllZj6DOz7QtEl7jI7syxjW6woS725ii4,5356
+sqlalchemy/dialects/sqlite/pysqlite.py,sha256=TAOqsHIjhbUZOF_Qk7UooiekkVZNhYJNduxlGQjokeA,27900
+sqlalchemy/dialects/type_migration_guidelines.txt,sha256=-uHNdmYFGB7bzUNT6i8M5nb4j6j9YUKAtW4lcBZqsMg,8239
+sqlalchemy/engine/__init__.py,sha256=Stb2oV6l8w65JvqEo6J4qtKoApcmOpXy3AAxQud4C1o,2818
+sqlalchemy/engine/_py_processors.py,sha256=j9i_lcYYQOYJMcsDerPxI0sVFBIlX5sqoYMdMJlgWPI,3744
+sqlalchemy/engine/_py_row.py,sha256=wSqoUFzLOJ1f89kgDb6sJm9LUrF5LMFpXPcK1vUsKcs,3787
+sqlalchemy/engine/_py_util.py,sha256=f2DI3AN1kv6EplelowesCVpwS8hSXNufRkZoQmJtSH8,2484
+sqlalchemy/engine/base.py,sha256=5n8SHmQh5Cr9DxGeRis12-_VQO4Cl7sM_FOnCBy2zD0,122207
+sqlalchemy/engine/characteristics.py,sha256=Qbvt4CPrggJ3GfxHl0hOAxopjnCQy-W_pjtwLIe-Q1g,2590
+sqlalchemy/engine/create.py,sha256=Lua52hd3e5H0a68rCVzrbwWZCnNKCef2Ew7FRchJK-c,32888
+sqlalchemy/engine/cursor.py,sha256=ErLMvqRMT8HJm-5RQeniC8-429cvXf8Sq3JSReLeVfI,74442
+sqlalchemy/engine/default.py,sha256=rZdc8JvEZCM4LtaWeJY3VsRrWMo_GDvfayK96nmGkvc,84065
+sqlalchemy/engine/events.py,sha256=c0unNFFiHzTAvkUtXoJaxzMFMDwurBkHiiUhuN8qluc,37381
+sqlalchemy/engine/interfaces.py,sha256=WE50MbuYGYhhMgF71GbfLVJAn2DDYEXiOOt6M3r-5z0,112814
+sqlalchemy/engine/mock.py,sha256=yvpxgFmRw5G4QsHeF-ZwQGHKES-HqQOucTxFtN1uzdk,4179
+sqlalchemy/engine/processors.py,sha256=XyfINKbo-2fjN-mW55YybvFyQMOil50_kVqsunahkNs,2379
+sqlalchemy/engine/reflection.py,sha256=FlT5kPpKm7Lah50GNt5XcnlJWojTL3LD_x0SoCF9kfY,75127
+sqlalchemy/engine/result.py,sha256=tGQX_zP5wlalOXXAV5BfljHd6bpSPePRXlju099TEe4,77756
+sqlalchemy/engine/row.py,sha256=S4d_WWD292B6_AnucKGG7E_6KFjEqP-BZAInrxGgicw,12080
+sqlalchemy/engine/strategies.py,sha256=DqFSWaXJPL-29Omot9O0aOcuGL8KmCGyOvnPGDkAJoE,442
+sqlalchemy/engine/url.py,sha256=dlbRISW9lMJ-Co_p1-TCXCBLMXGyrv7ROgll26FTe74,30558
+sqlalchemy/engine/util.py,sha256=hkEql1t19WHl6uzR55-F-Fs_VMCJ7p02KKQVNUDSXTk,5667
+sqlalchemy/event/__init__.py,sha256=KBrp622xojnC3FFquxa2JsMamwAbfkvzfv6Op0NKiYc,997
+sqlalchemy/event/api.py,sha256=BUTAZjSlzvq4Hn2v2pihP_P1yo3lvCVDczK8lV_XJ80,8227
+sqlalchemy/event/attr.py,sha256=h8pFjHgyvLTeai6_LRQYk6ii-6M3PGgXFeZlPe-OJgo,20767
+sqlalchemy/event/base.py,sha256=LFcvzFaop51Im1IP1vz3BxFqQWXA90F2b4CMEV3JyKI,14980
+sqlalchemy/event/legacy.py,sha256=I5e9JLpRybIVlRi_ArEwymwbfx8vNns52v9QWaqVtA4,8211
+sqlalchemy/event/registry.py,sha256=LKbNsF5quf0DAGCw8jaGLmSf7p9ejWjT2y_Oz7FoDNo,10833
+sqlalchemy/events.py,sha256=k-ZD38aSPD29LYhED7CBqttp5MDVVx_YSaWC2-cu9ec,525
+sqlalchemy/exc.py,sha256=GRMcfOg64pRXX7nGsDt5iXfwqUdiMILstYse87vzmLI,24000
+sqlalchemy/ext/__init__.py,sha256=S1fGKAbycnQDV01gs-JWGaFQ9GCD4QHwKcU2wnugg_o,322
+sqlalchemy/ext/associationproxy.py,sha256=BPYsvBlh8KVxKPLhHc31_JTatk7bhrSJTqO50b3OLcA,65960
+sqlalchemy/ext/asyncio/__init__.py,sha256=1OqSxEyIUn7RWLGyO12F-jAUIvk1I6DXlVy80-Gvkds,1317
+sqlalchemy/ext/asyncio/base.py,sha256=HVpevdn2vcCrhWyeSDdP0JFm2SdEIitmBlXv82Yywbo,8937
+sqlalchemy/ext/asyncio/engine.py,sha256=vy8_HiMFP4HWXvqLSES0AMQIWWNUmrIxPIeVHa6QbEw,48058
+sqlalchemy/ext/asyncio/exc.py,sha256=8sII7VMXzs2TrhizhFQMzSfcroRtiesq8o3UwLfXSgQ,639
+sqlalchemy/ext/asyncio/result.py,sha256=pVBeJym7zjT4eMcduU3X2_g5qtnaKuy2kIJWVJGTv_o,30554
+sqlalchemy/ext/asyncio/scoping.py,sha256=xLdjNJ1VnlTmi5YAsZKQo3XfBzKpviKpLG_HeSSbv2g,52685
+sqlalchemy/ext/asyncio/session.py,sha256=sc9SKwqEgPiTIjlIyVa679F-Q2CWp6_8ucJOis_hYaQ,62998
+sqlalchemy/ext/automap.py,sha256=MTvMs97xALDugdgyY3JqiVeusrcKruN0BR7iSFhNZOg,61431
+sqlalchemy/ext/baked.py,sha256=H6T1il7GY84BhzPFj49UECSpZh_eBuiHomA-QIsYOYQ,17807
+sqlalchemy/ext/compiler.py,sha256=ONPoxoKD2yUS9R2-oOhmPsA7efm-Bs0BXo7HE1dGlsU,20391
+sqlalchemy/ext/declarative/__init__.py,sha256=20psLdFQbbOWfpdXHZ0CTY6I1k4UqXvKemNVu1LvPOI,1818
+sqlalchemy/ext/declarative/extensions.py,sha256=uCjN1GisQt54AjqYnKYzJdUjnGd2pZBW47WWdPlS7FE,19547
+sqlalchemy/ext/horizontal_shard.py,sha256=ITc2MU4pVc6t_HLR-T6tTMvYbwrl8cM9Bi02jI4e7FM,16766
+sqlalchemy/ext/hybrid.py,sha256=iU2GCE-PDHWa6hp3-nv9fNjuk46hzmRPJKxHEs7smxY,52514
+sqlalchemy/ext/indexable.py,sha256=UkTelbydKCdKelzbv3HWFFavoET9WocKaGRPGEOVfN8,11032
+sqlalchemy/ext/instrumentation.py,sha256=7908PLZGlD9tiq0nWS0A7iNNax0iGNsIchhJ_OoxOCg,15723
+sqlalchemy/ext/mutable.py,sha256=IX_H7vCBG834gFtCQzopuwgpmEDspBQBdb_nK-Dfrfo,37427
+sqlalchemy/ext/mypy/__init__.py,sha256=0WebDIZmqBD0OTq5JLtd_PmfF9JGxe4d4Qv3Ml3PKUg,241
+sqlalchemy/ext/mypy/apply.py,sha256=KZP0RAsQd65hyoS9bOBZ0UtgA1sZSd1wH9S0jyc_v08,10508
+sqlalchemy/ext/mypy/decl_class.py,sha256=gQOIMZOpKKdFW1TQjnEVM2RxGJbZzz5dFeMWRaIoFHc,17382
+sqlalchemy/ext/mypy/infer.py,sha256=KVnmLFEVS33Al8pUKI7MJbJQu3KeveBUMl78EluBORw,19369
+sqlalchemy/ext/mypy/names.py,sha256=IQ16GLZFqKxfYxIZxkbTurBqOUYbUV-64V_DSRns1tc,10630
+sqlalchemy/ext/mypy/plugin.py,sha256=74ML8LI9xar0V86oCxnPFv5FQGEEfUzK64vOay4BKFs,9750
+sqlalchemy/ext/mypy/util.py,sha256=DLvKhM38mZk_1vvxbpenq-krTwdqdrZbg-o072TodCs,9408
+sqlalchemy/ext/orderinglist.py,sha256=TGYbsGH72wEZcFNQDYDsZg9OSPuzf__P8YX8_2HtYUo,14384
+sqlalchemy/ext/serializer.py,sha256=YemanWdeMVUDweHCnQc-iMO6mVVXNo2qQ5NK0Eb2_Es,6178
+sqlalchemy/future/__init__.py,sha256=q2mw-gxk_xoxJLEvRoyMha3vO1xSRHrslcExOHZwmPA,512
+sqlalchemy/future/engine.py,sha256=AgIw6vMsef8W6tynOTkxsjd6o_OQDwGjLdbpoMD8ue8,495
+sqlalchemy/inspection.py,sha256=fLQyZnIS7irH7dRX3AaUHCkCArjIxMboaJj0rjM3cnA,5137
+sqlalchemy/log.py,sha256=KPxnFg6D2kj2_-3k6njiwxqbqyj51lid-O8ZWZN3HIg,8623
+sqlalchemy/orm/__init__.py,sha256=ZYys5nL3RFUDCMOLFDBrRI52F6er3S1U1OY9TeORuKs,8463
+sqlalchemy/orm/_orm_constructors.py,sha256=hhvHmDbY2cbMedLbLRYVAh14XO1U1Co513qzIke0THY,99370
+sqlalchemy/orm/_typing.py,sha256=RhN8pyEBjpYzGaxezyZCPRNV82U--BMRTIDdIiogsyo,5024
+sqlalchemy/orm/attributes.py,sha256=2R9p508nfGDwj_qr4Hxbv_g2QDDQSp0-n_GCHcjqb2U,92578
+sqlalchemy/orm/base.py,sha256=4-AzRGLdOTCOpgg8larSWb6s-YOqbMLrgQPhw__51ys,27700
+sqlalchemy/orm/bulk_persistence.py,sha256=vjx8DSvbeyTzDiuwaBonZ8TDc6kfUtBIEaXJZEuY6ac,69878
+sqlalchemy/orm/clsregistry.py,sha256=W9Su2JwNCvJuJiAyOT49ALUj59G36z4ugJimkKId530,17962
+sqlalchemy/orm/collections.py,sha256=t184F_YbXxZdwLOyv85l4M32gbJ-EQtbdMewnsmfdHg,52159
+sqlalchemy/orm/context.py,sha256=esaaJ7pYE4-SzXqeEE-Pv7BZLtnghwMY2dasW9N1vnY,111893
+sqlalchemy/orm/decl_api.py,sha256=YrN1zaL7yFAXFtzJYkzC6DUMaqfEenCdQnBGSK1Xn-E,63882
+sqlalchemy/orm/decl_base.py,sha256=WfKe0wyxrYr1-mmeiQgfIhRD_PHlFK-xDNsvYJ3R0T0,81621
+sqlalchemy/orm/dependency.py,sha256=lrTu8yfqLbz7U0iOHULR_Yk4C0z-2VDtFpuH7TaeynA,47583
+sqlalchemy/orm/descriptor_props.py,sha256=RennfXQ7bdnANgNF2SlB_-W_GlaZ26SsdhI4Yb_orYE,37176
+sqlalchemy/orm/dynamic.py,sha256=toSmHi9AF9nnbZmvLSMOSGR0NaG0YpszlLQx8KnxMbk,9798
+sqlalchemy/orm/evaluator.py,sha256=q292K5vdpP69G7Z9y1RqI5GFAk2diUPwnsXE8De_Wgw,11925
+sqlalchemy/orm/events.py,sha256=USrIP-2JlcIbmssvCkea1veL3eIIWC7WH7KDmTzqa-Q,127601
+sqlalchemy/orm/exc.py,sha256=w7MZkJMGGlu5J6jOFSmi9XXzc02ctnTv34jrEWpI-eM,7356
+sqlalchemy/orm/identity.py,sha256=jHdCxCpCyda_8mFOfGmN_Pr0XZdKiU-2hFZshlNxbHs,9249
+sqlalchemy/orm/instrumentation.py,sha256=wHdGTYpzND7nhgbpmiryLOXuWLIzCVii6jpfVWAi2RQ,24337
+sqlalchemy/orm/interfaces.py,sha256=0cK5udFIGusQ3cW697zXEElIPVafxW5UD5KL_6oNi8w,48404
+sqlalchemy/orm/loading.py,sha256=GKvLzmFklYgS89enTt_b2fvyDnu9rUAlYLL-Oa9eAeM,57417
+sqlalchemy/orm/mapped_collection.py,sha256=IsSSxYWuR396Qep4MYCe2VDmFr5sU_3AuDrhXDf_mVA,19704
+sqlalchemy/orm/mapper.py,sha256=j5r6ezZKW_WOcp_SIz0neMHMXiAMriVMdXbgvIhusfg,170969
+sqlalchemy/orm/path_registry.py,sha256=uZULekFBpy292nYuE0ON6vGcgM0Szs6a0iN7Wyya0_A,25938
+sqlalchemy/orm/persistence.py,sha256=2nQZpi9Mi-uKlJ-cwLodOMu-9gs4ZpYcUseIk4T210M,60989
+sqlalchemy/orm/properties.py,sha256=1gaf8QaGunBN2K2nEvMcucD4U1cOntJgsqJafLtHi7w,29095
+sqlalchemy/orm/query.py,sha256=QwUV1vm-6gyriYiZ4GIoNV3rdwdaCFhfdChTvVq00Oo,117714
+sqlalchemy/orm/relationships.py,sha256=NRQBABfdAWWqtRzhyE8Xq_uDeFPLxruSBNK646yv-vo,127619
+sqlalchemy/orm/scoping.py,sha256=Aeu34zEhcxcbS8NCzfgHzDBhlSXnlay5Ln8SPczdh9k,78821
+sqlalchemy/orm/session.py,sha256=bJTTXE7yB4qD6JSe0lvJv8QASf2OoUOoHQplt1ODHJk,193265
+sqlalchemy/orm/state.py,sha256=M60-bI0R0dzGHc-fBNGXX1V8osNTSC_WwtmlhcErSBM,37536
+sqlalchemy/orm/state_changes.py,sha256=qKYg7NxwrDkuUY3EPygAztym6oAVUFcP2wXn7QD3Mz4,6815
+sqlalchemy/orm/strategies.py,sha256=u6d7F5cAi2TC4NNYzFecoMYEUownpQcght5hIXVPJ7M,114052
+sqlalchemy/orm/strategy_options.py,sha256=267SGNfWJGlWjrdBffX8cQYrN7Ilk3Xk4kL4J150P7U,84161
+sqlalchemy/orm/sync.py,sha256=g7iZfSge1HgxMk9SKRgUgtHEbpbZ1kP_CBqOIdTOXqc,5779
+sqlalchemy/orm/unitofwork.py,sha256=fiVaqcymbDDHRa1NjS90N9Z466nd5pkJOEi1dHO6QLY,27033
+sqlalchemy/orm/util.py,sha256=j6BLkPtzZs88tINO21h-Dv3w2N1mlRZNabB_Q27U9ac,80340
+sqlalchemy/orm/writeonly.py,sha256=xh-KN8CiykLNQ_L9HE8QM1A822CJfiYopG9snSbWsz0,22329
+sqlalchemy/pool/__init__.py,sha256=qiDdq4r4FFAoDrK6ncugF_i6usi_X1LeJt-CuBHey0s,1804
+sqlalchemy/pool/base.py,sha256=2c614izCvoBw0xxu0LOhtziVB_G_MGns0jZu6ts9Bn8,52243
+sqlalchemy/pool/events.py,sha256=12gjivfkZkROTFYoIUS0kkvt3Ftj5ogBGavP9oSFPl4,13137
+sqlalchemy/pool/impl.py,sha256=2gdX23oZPLEqf3phI8yJod4ElB1BwFWkZCAtEXjXqbM,17718
+sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+sqlalchemy/schema.py,sha256=dKiWmgHYjcKQ4TiiD6vD0UMmIsD8u0Fsor1M9AAeGUs,3194
+sqlalchemy/sql/__init__.py,sha256=UNa9EUiYWoPayf-FzNcwVgQvpsBdInPZfpJesAStN9o,5820
+sqlalchemy/sql/_dml_constructors.py,sha256=YdBJex0MCVACv4q2nl_ii3uhxzwU6aDB8zAsratX5UQ,3867
+sqlalchemy/sql/_elements_constructors.py,sha256=oCro-h7QqOIbq4abduZP8EIw88HbHhPWPdyN8fzMuas,62558
+sqlalchemy/sql/_orm_types.py,sha256=T-vjcry4C1y0GToFKVxQCnmly_-Zsq4IO4SHN6bvUF4,625
+sqlalchemy/sql/_py_util.py,sha256=hiM9ePbRSGs60bAMxPFuJCIC_p9SQ1VzqXGiPchiYwE,2173
+sqlalchemy/sql/_selectable_constructors.py,sha256=kHm45Q6t2QCHIj2CS0SEZvD0MwKmK59lmg55NQMMKK8,18812
+sqlalchemy/sql/_typing.py,sha256=XDr2i-6GxPLBvtJC3ybzffQubUv_h3I2xRDUBlQLg30,12613
+sqlalchemy/sql/annotation.py,sha256=fEeyIJYId-xVHxp4VuHUzOnosa3iKLAHExnb8swq8v0,18271
+sqlalchemy/sql/base.py,sha256=vnkJFb9yJwTFX9Fd0CsISiP9m3PBfNQiBfMCmfUt69k,73928
+sqlalchemy/sql/cache_key.py,sha256=mTIa7UfTiUpeXDcVqn5vDBf-soj81fXOIHLPNjoQzhg,33124
+sqlalchemy/sql/coercions.py,sha256=BKj_pkSS4zBmc3tiS8wWmNhGWeyr66sK4QHJudqy1Lg,40489
+sqlalchemy/sql/compiler.py,sha256=WgnzXhjGvHxEC4VhIXgoeCoyKDFjRGfIwv0rH4Ug6vw,269842
+sqlalchemy/sql/crud.py,sha256=Xp3rX7N-YuecL14StUEc959a6oj61EG_fiyoiVYGAqY,56457
+sqlalchemy/sql/ddl.py,sha256=GJfH800KdwMhBbhp1f6FFyDVx-RjtG2WRGa8UTVaZI4,45542
+sqlalchemy/sql/default_comparator.py,sha256=1OiYbEojh6Vq8gy_jqY3b19dYZ0DvjFbyV5QeFiAs_o,16646
+sqlalchemy/sql/dml.py,sha256=BXUkqWPhnELKrhRKhfWZH4YZXKCCDCoHemVnfaew8Us,65728
+sqlalchemy/sql/elements.py,sha256=sv1D2nLZO0rmnmcrPuQcMAp2YCkTWv_r4GBG_xDlSbY,172784
+sqlalchemy/sql/events.py,sha256=iC_Q1Htm1Aobt5tOYxWfHHqNpoytrULORmUKcusH_-E,18290
+sqlalchemy/sql/expression.py,sha256=VMX-dLpsZYnVRJpYNDozDUgaj7iQ0HuewUKVefD57PE,7586
+sqlalchemy/sql/functions.py,sha256=e_29NAfsMBltrVVlgVUDt2CizHXrtS8dVBzvqnXgMJg,64248
+sqlalchemy/sql/lambdas.py,sha256=bG3D175kcQ3-9OWMLABSa6-diJyOfMf0ajr2w4mFdfM,49281
+sqlalchemy/sql/naming.py,sha256=ZHs1qSV3ou8TYmZ92uvU3sfdklUQlIz4uhe330n05SU,6858
+sqlalchemy/sql/operators.py,sha256=eCADkzisFq3PPWUSN28HYe8sgpxFQe6kJUATPn-XsuM,76193
+sqlalchemy/sql/roles.py,sha256=VwwJacCiopxIHABAeNgLApDxo-xHPhJl7UfdLebMJGw,7686
+sqlalchemy/sql/schema.py,sha256=XO0fxfaBbnyJjlunZln_Xi8ecPM_l7WZowgyi-btAek,228211
+sqlalchemy/sql/selectable.py,sha256=_Bu-nSxEC3kSUosAsDsZZn7S7BC_HtbFainREIQK6D8,233041
+sqlalchemy/sql/sqltypes.py,sha256=Bv_xlg27fsYN0xPQ81LaQ-snVm2JqsX_Rz1IpkUBqpw,126509
+sqlalchemy/sql/traversals.py,sha256=Tvt6DFCGIqsQHSCfKzk2kAbi_bnEX3jh_8ZRdtOFoYE,33521
+sqlalchemy/sql/type_api.py,sha256=PhMBSDza_dT5RXSPNkpTJUqGj2ojI_2vSkOSWl5OuRQ,83883
+sqlalchemy/sql/util.py,sha256=LVLjLqpZuJ_DT6oUeE7WJbuCkIMEA8ZUEbO204Gp06U,48187
+sqlalchemy/sql/visitors.py,sha256=St-h4A5ZMVMGu9hFhbGZJUxa9KaEg7e2KaCMihVTU64,36427
+sqlalchemy/testing/__init__.py,sha256=VsrEHrORpAF5n7Vfl43YQgABh6EP1xBx_gHxs7pSXeE,3126
+sqlalchemy/testing/assertions.py,sha256=gL0rA7CCZJbcVgvWOPV91tTZTRwQc1_Ta0-ykBn83Ew,31439
+sqlalchemy/testing/assertsql.py,sha256=xwo0ZuCN69Y0ElCZys3lnmPxHdmJ34E2Cns4V3g8MA0,16817
+sqlalchemy/testing/asyncio.py,sha256=fkdRz-E37d5OrQKw5hdjmglOTJyXGnJzaJpvNXOBLxg,3728
+sqlalchemy/testing/config.py,sha256=9HWOgvPLSRJIjRWa0wauo3klYafV9oEhp8qhjptvlVw,12030
+sqlalchemy/testing/engines.py,sha256=iB3dLHhSBLPbTB5lSYnnppnfB66IP2DmfW56mhH4sjI,13355
+sqlalchemy/testing/entities.py,sha256=IphFegPKbff3Un47jY6bi7_MQXy6qkx_50jX2tHZJR4,3354
+sqlalchemy/testing/exclusions.py,sha256=vZlqF8Jy_PpXc9e-yPAI8-UHFVM-UiRfMwI-WrWS-nU,12444
+sqlalchemy/testing/fixtures/__init__.py,sha256=dMClrIoxqlYIFpk2ia4RZpkbfxsS_3EBigr9QsPJ66g,1198
+sqlalchemy/testing/fixtures/base.py,sha256=9r_J2ksiTzClpUxW0TczICHrWR7Ny8PV8IsBz6TsGFI,12256
+sqlalchemy/testing/fixtures/mypy.py,sha256=nrfgQnzIZoRFJ47F-7IZpouvAq6mSQHb8A-TbiDxv5I,11845
+sqlalchemy/testing/fixtures/orm.py,sha256=8EFbnaBbXX_Bf4FcCzBUaAHgyVpsLGBHX16SGLqE3Fg,6095
+sqlalchemy/testing/fixtures/sql.py,sha256=MFOuYBUyPIpHJzjRCHL9vU-IT4bD6LXGGMvsp0v1FY8,15704
+sqlalchemy/testing/pickleable.py,sha256=U9mIqk-zaxq9Xfy7HErP7UrKgTov-A3QFnhZh-NiOjI,2833
+sqlalchemy/testing/plugin/__init__.py,sha256=79F--BIY_NTBzVRIlJGgAY5LNJJ3cD19XvrAo4X0W9A,247
+sqlalchemy/testing/plugin/bootstrap.py,sha256=oYScMbEW4pCnWlPEAq1insFruCXFQeEVBwo__i4McpU,1685
+sqlalchemy/testing/plugin/plugin_base.py,sha256=IR2tLVvW7dbAqagFYwUjsc2X1oVIRVp1GM_b6tZTQkw,21581
+sqlalchemy/testing/plugin/pytestplugin.py,sha256=xasjXXEMsT4RtKR7WzmEqzYXPxz83KSRaM2CwJQEQK8,27546
+sqlalchemy/testing/profiling.py,sha256=PbuPhRFbauFilUONeY3tV_Y_5lBkD7iCa8VVyH2Sk9Y,10148
+sqlalchemy/testing/provision.py,sha256=zXsw2D2Xpmw_chmYLsE1GXQqKQ-so3V8xU_joTcKan0,14619
+sqlalchemy/testing/requirements.py,sha256=N9pSj7z2wVMkBif-DQfPVa_cl9k6p9g_J5FY1OsWtrY,51817
+sqlalchemy/testing/schema.py,sha256=lr4GkGrGwagaHMuSGzWdzkMaj3HnS7dgfLLWfxt__-U,6513
+sqlalchemy/testing/suite/__init__.py,sha256=Y5DRNG0Yl1u3ypt9zVF0Z9suPZeuO_UQGLl-wRgvTjU,722
+sqlalchemy/testing/suite/test_cte.py,sha256=6zBC3W2OwX1Xs-HedzchcKN2S7EaLNkgkvV_JSZ_Pq0,6451
+sqlalchemy/testing/suite/test_ddl.py,sha256=1Npkf0C_4UNxphthAGjG078n0vPEgnSIHpDu5MfokxQ,12031
+sqlalchemy/testing/suite/test_deprecations.py,sha256=BcJxZTcjYqeOAENVElCg3hVvU6fkGEW3KGBMfnW8bng,5337
+sqlalchemy/testing/suite/test_dialect.py,sha256=EH4ZQWbnGdtjmx5amZtTyhYmrkXJCvW1SQoLahoE7uk,22923
+sqlalchemy/testing/suite/test_insert.py,sha256=8ASo87s2pvaFkYZbmx5zYDGsRloyGpI9Zo5ut4ttM9g,18557
+sqlalchemy/testing/suite/test_reflection.py,sha256=_44jrB9iQ0PbQ3Aj2nz45eJIkDmCGJ5I8r4Q2-Km9Ww,106458
+sqlalchemy/testing/suite/test_results.py,sha256=NQ23m8FDVd0ub751jN4PswGoAhk5nrqvjHvpYULZXnc,15937
+sqlalchemy/testing/suite/test_rowcount.py,sha256=Ozu9NmGrsMWROGNdtE-KKOaa_WMHnvSy-qcnAYpG20Y,7903
+sqlalchemy/testing/suite/test_select.py,sha256=FvMFYQW9IJpDWGYZiJk46is6YrtmdSghBdTjZCG8T0Y,58574
+sqlalchemy/testing/suite/test_sequence.py,sha256=66bCoy4xo99GBSaX6Hxb88foANAykLGRz1YEKbvpfuA,9923
+sqlalchemy/testing/suite/test_types.py,sha256=rFmTOg6XuMch9L2-XthfLJRCTTwpZbMfrNss2g09gmc,65677
+sqlalchemy/testing/suite/test_unicode_ddl.py,sha256=c3_eIxLyORuSOhNDP0jWKxPyUf3SwMFpdalxtquwqlM,6141
+sqlalchemy/testing/suite/test_update_delete.py,sha256=3aaM9LtV_qC8NzMcOn2BZHxnQ5eWJnFKOx6iQpGArSw,3914
+sqlalchemy/testing/util.py,sha256=BFiSp3CEX95Dr-vv4l_7ZRu5vjZi9hjjnp-JKNfuS5E,14080
+sqlalchemy/testing/warnings.py,sha256=fJ-QJUY2zY2PPxZJKv9medW-BKKbCNbA4Ns_V3YwFXM,1546
+sqlalchemy/types.py,sha256=cQFM-hFRmaf1GErun1qqgEs6QxufvzMuwKqj9tuMPpE,3168
+sqlalchemy/util/__init__.py,sha256=B3bedg-LSQEscwqgmYYU-VENUX8_zAE3q9vb7tkfJNY,8277
+sqlalchemy/util/_collections.py,sha256=py6nwBAgPC_pmbghrobaCVZZxmiVuLgqrbjdgqP_3jU,20111
+sqlalchemy/util/_concurrency_py3k.py,sha256=3jieKUZN8sS-RS6L0a2BUCUm_pqQMldbuFCxZ7xr1-Q,8602
+sqlalchemy/util/_has_cy.py,sha256=wCQmeSjT3jaH_oxfCEtGk-1g0gbSpt5MCK5UcWdMWqk,1247
+sqlalchemy/util/_py_collections.py,sha256=AZEigbk8jfiKjH3Rqy6BBmRwe8csZuRZ5lz2DYS-Cls,16738
+sqlalchemy/util/compat.py,sha256=R6bpBydldtbr6h7oJePihQxFb7jKiI-YDsK465MSOzk,8714
+sqlalchemy/util/concurrency.py,sha256=mhwHm0utriD14DRqxTBWgIW7QuwdSEiLgLiJdUjiR3w,2427
+sqlalchemy/util/deprecations.py,sha256=YBwvvYhSB8LhasIZRKvg_-WNoVhPUcaYI1ZrnjDn868,11971
+sqlalchemy/util/langhelpers.py,sha256=HEdM_PcZxoSXH9qlcvf5Vp1k4xKU2XJBwn0Aq9RVmeU,64980
+sqlalchemy/util/preloaded.py,sha256=az7NmLJLsqs0mtM9uBkIu10-841RYDq8wOyqJ7xXvqE,5904
+sqlalchemy/util/queue.py,sha256=_5qHVIvWluQQt0jiDHnxuT2FhUxnxH2cKDxCmwmTFe0,10205
+sqlalchemy/util/tool_support.py,sha256=9braZyidaiNrZVsWtGmkSmus50-byhuYrlAqvhjcmnA,6135
+sqlalchemy/util/topological.py,sha256=N3M3Le7KzGHCmqPGg0ZBqixTDGwmFLhOZvBtc4rHL_g,3458
+sqlalchemy/util/typing.py,sha256=-YKRlPyicpM94qhFAUj9eYnv2hnKHkhv0Gxq7jNPAvE,16255
diff --git a/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/REQUESTED b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/REQUESTED
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/WHEEL b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/WHEEL
new file mode 100644
index 00000000..876de0f5
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.42.0)
+Root-Is-Purelib: false
+Tag: cp39-cp39-macosx_11_0_arm64
+
diff --git a/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/top_level.txt b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/top_level.txt
new file mode 100644
index 00000000..39fb2bef
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/SQLAlchemy-2.0.25.dist-info/top_level.txt
@@ -0,0 +1 @@
+sqlalchemy
diff --git a/backend/venv/lib/python3.9/site-packages/_argon2_cffi_bindings/__init__.py b/backend/venv/lib/python3.9/site-packages/_argon2_cffi_bindings/__init__.py
new file mode 100644
index 00000000..8def07a3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/_argon2_cffi_bindings/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: MIT
+
+from ._ffi import ffi, lib
+
+
+__all__ = ["ffi", "lib"]
diff --git a/backend/venv/lib/python3.9/site-packages/_argon2_cffi_bindings/_ffi.abi3.so b/backend/venv/lib/python3.9/site-packages/_argon2_cffi_bindings/_ffi.abi3.so
new file mode 100755
index 00000000..99fbc54b
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/_argon2_cffi_bindings/_ffi.abi3.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/_argon2_cffi_bindings/_ffi_build.py b/backend/venv/lib/python3.9/site-packages/_argon2_cffi_bindings/_ffi_build.py
new file mode 100644
index 00000000..ec4c8238
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/_argon2_cffi_bindings/_ffi_build.py
@@ -0,0 +1,233 @@
+# SPDX-License-Identifier: MIT
+
+import os
+import platform
+import sysconfig
+
+from pathlib import Path
+
+from cffi import FFI
+
+
+use_system_argon2 = os.environ.get("ARGON2_CFFI_USE_SYSTEM", "0") == "1"
+use_sse2 = os.environ.get("ARGON2_CFFI_USE_SSE2", None)
+windows = platform.system() == "Windows"
+# Free-threaded CPython doesn't support limited API.
+limited_api = not sysconfig.get_config_var("Py_GIL_DISABLED")
+
+
+# Try to detect cross-compilation.
+def _get_target_platform(arch_flags, default):
+ flags = [f for f in arch_flags.split(" ") if f.strip() != ""]
+ try:
+ pos = flags.index("-arch")
+
+ return flags[pos + 1].lower()
+ except ValueError:
+ pass
+
+ return default
+
+
+target_platform = _get_target_platform(
+ os.environ.get("ARCHFLAGS", ""), platform.machine()
+)
+
+
+if use_sse2 == "1":
+ optimized = True
+elif use_sse2 == "0":
+ optimized = False
+else:
+ # Optimized version requires SSE2 extensions. They have been around since
+ # 2001 so we try to compile it on every recent-ish x86.
+ optimized = target_platform in ("i686", "x86", "x86_64", "AMD64")
+
+
+ffi = FFI()
+
+if use_system_argon2:
+ ffi.set_source(
+ "_ffi",
+ "#include ",
+ libraries=["argon2"],
+ py_limited_api=limited_api,
+ )
+else:
+ lib_base = Path("extras") / "libargon2" / "src"
+ ffi.set_source(
+ "_ffi",
+ "#include ",
+ extra_compile_args=["-msse2"] if (optimized and not windows) else None,
+ include_dirs=[str(Path("extras", "libargon2", "include"))],
+ py_limited_api=limited_api,
+ sources=[
+ str(lib_base / path)
+ for path in [
+ "argon2.c",
+ Path("blake2") / "blake2b.c",
+ "core.c",
+ "encoding.c",
+ "opt.c" if optimized else "ref.c",
+ "thread.c",
+ ]
+ ],
+ )
+
+
+ffi.cdef(
+ """\
+typedef enum Argon2_type {
+ Argon2_d = ...,
+ Argon2_i = ...,
+ Argon2_id = ...,
+} argon2_type;
+typedef enum Argon2_version {
+ ARGON2_VERSION_10 = ...,
+ ARGON2_VERSION_13 = ...,
+ ARGON2_VERSION_NUMBER = ...
+} argon2_version;
+
+int argon2_hash(const uint32_t t_cost, const uint32_t m_cost,
+ const uint32_t parallelism, const void *pwd,
+ const size_t pwdlen, const void *salt,
+ const size_t saltlen, void *hash,
+ const size_t hashlen, char *encoded,
+ const size_t encodedlen, argon2_type type,
+ const uint32_t version);
+
+int argon2_verify(const char *encoded, const void *pwd,
+ const size_t pwdlen, argon2_type type);
+
+const char *argon2_error_message(int error_code);
+
+
+typedef int (*allocate_fptr)(uint8_t **memory, size_t bytes_to_allocate);
+typedef void (*deallocate_fptr)(uint8_t *memory, size_t bytes_to_allocate);
+
+typedef struct Argon2_Context {
+ uint8_t *out; /* output array */
+ uint32_t outlen; /* digest length */
+
+ uint8_t *pwd; /* password array */
+ uint32_t pwdlen; /* password length */
+
+ uint8_t *salt; /* salt array */
+ uint32_t saltlen; /* salt length */
+
+ uint8_t *secret; /* key array */
+ uint32_t secretlen; /* key length */
+
+ uint8_t *ad; /* associated data array */
+ uint32_t adlen; /* associated data length */
+
+ uint32_t t_cost; /* number of passes */
+ uint32_t m_cost; /* amount of memory requested (KB) */
+ uint32_t lanes; /* number of lanes */
+ uint32_t threads; /* maximum number of threads */
+
+ uint32_t version; /* version number */
+
+ allocate_fptr allocate_cbk; /* pointer to memory allocator */
+ deallocate_fptr free_cbk; /* pointer to memory deallocator */
+
+ uint32_t flags; /* array of bool options */
+} argon2_context;
+
+int argon2_ctx(argon2_context *context, argon2_type type);
+
+/* Error codes */
+typedef enum Argon2_ErrorCodes {
+ ARGON2_OK = ...,
+
+ ARGON2_OUTPUT_PTR_NULL = ...,
+
+ ARGON2_OUTPUT_TOO_SHORT = ...,
+ ARGON2_OUTPUT_TOO_LONG = ...,
+
+ ARGON2_PWD_TOO_SHORT = ...,
+ ARGON2_PWD_TOO_LONG = ...,
+
+ ARGON2_SALT_TOO_SHORT = ...,
+ ARGON2_SALT_TOO_LONG = ...,
+
+ ARGON2_AD_TOO_SHORT = ...,
+ ARGON2_AD_TOO_LONG = ...,
+
+ ARGON2_SECRET_TOO_SHORT = ...,
+ ARGON2_SECRET_TOO_LONG = ...,
+
+ ARGON2_TIME_TOO_SMALL = ...,
+ ARGON2_TIME_TOO_LARGE = ...,
+
+ ARGON2_MEMORY_TOO_LITTLE = ...,
+ ARGON2_MEMORY_TOO_MUCH = ...,
+
+ ARGON2_LANES_TOO_FEW = ...,
+ ARGON2_LANES_TOO_MANY = ...,
+
+ ARGON2_PWD_PTR_MISMATCH = ..., /* NULL ptr with non-zero length */
+ ARGON2_SALT_PTR_MISMATCH = ..., /* NULL ptr with non-zero length */
+ ARGON2_SECRET_PTR_MISMATCH = ..., /* NULL ptr with non-zero length */
+ ARGON2_AD_PTR_MISMATCH = ..., /* NULL ptr with non-zero length */
+
+ ARGON2_MEMORY_ALLOCATION_ERROR = ...,
+
+ ARGON2_FREE_MEMORY_CBK_NULL = ...,
+ ARGON2_ALLOCATE_MEMORY_CBK_NULL = ...,
+
+ ARGON2_INCORRECT_PARAMETER = ...,
+ ARGON2_INCORRECT_TYPE = ...,
+
+ ARGON2_OUT_PTR_MISMATCH = ...,
+
+ ARGON2_THREADS_TOO_FEW = ...,
+ ARGON2_THREADS_TOO_MANY = ...,
+
+ ARGON2_MISSING_ARGS = ...,
+
+ ARGON2_ENCODING_FAIL = ...,
+
+ ARGON2_DECODING_FAIL = ...,
+
+ ARGON2_THREAD_FAIL = ...,
+
+ ARGON2_DECODING_LENGTH_FAIL= ...,
+
+ ARGON2_VERIFY_MISMATCH = ...,
+} argon2_error_codes;
+
+#define ARGON2_FLAG_CLEAR_PASSWORD ...
+#define ARGON2_FLAG_CLEAR_SECRET ...
+#define ARGON2_DEFAULT_FLAGS ...
+
+#define ARGON2_MIN_LANES ...
+#define ARGON2_MAX_LANES ...
+#define ARGON2_MIN_THREADS ...
+#define ARGON2_MAX_THREADS ...
+#define ARGON2_SYNC_POINTS ...
+#define ARGON2_MIN_OUTLEN ...
+#define ARGON2_MAX_OUTLEN ...
+#define ARGON2_MIN_MEMORY ...
+#define ARGON2_MAX_MEMORY_BITS ...
+#define ARGON2_MAX_MEMORY ...
+#define ARGON2_MIN_TIME ...
+#define ARGON2_MAX_TIME ...
+#define ARGON2_MIN_PWD_LENGTH ...
+#define ARGON2_MAX_PWD_LENGTH ...
+#define ARGON2_MIN_AD_LENGTH ...
+#define ARGON2_MAX_AD_LENGTH ...
+#define ARGON2_MIN_SALT_LENGTH ...
+#define ARGON2_MAX_SALT_LENGTH ...
+#define ARGON2_MIN_SECRET ...
+#define ARGON2_MAX_SECRET ...
+
+uint32_t argon2_encodedlen(uint32_t t_cost, uint32_t m_cost,
+ uint32_t parallelism, uint32_t saltlen,
+ uint32_t hashlen, argon2_type type);
+
+"""
+)
+
+if __name__ == "__main__":
+ ffi.compile()
diff --git a/backend/venv/lib/python3.9/site-packages/_cffi_backend.cpython-39-darwin.so b/backend/venv/lib/python3.9/site-packages/_cffi_backend.cpython-39-darwin.so
new file mode 100755
index 00000000..fa6a7134
Binary files /dev/null and b/backend/venv/lib/python3.9/site-packages/_cffi_backend.cpython-39-darwin.so differ
diff --git a/backend/venv/lib/python3.9/site-packages/_distutils_hack/__init__.py b/backend/venv/lib/python3.9/site-packages/_distutils_hack/__init__.py
new file mode 100644
index 00000000..5f40996a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/_distutils_hack/__init__.py
@@ -0,0 +1,128 @@
+import sys
+import os
+import re
+import importlib
+import warnings
+
+
+is_pypy = '__pypy__' in sys.builtin_module_names
+
+
+warnings.filterwarnings('ignore',
+ r'.+ distutils\b.+ deprecated',
+ DeprecationWarning)
+
+
+def warn_distutils_present():
+ if 'distutils' not in sys.modules:
+ return
+ if is_pypy and sys.version_info < (3, 7):
+ # PyPy for 3.6 unconditionally imports distutils, so bypass the warning
+ # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
+ return
+ warnings.warn(
+ "Distutils was imported before Setuptools, but importing Setuptools "
+ "also replaces the `distutils` module in `sys.modules`. This may lead "
+ "to undesirable behaviors or errors. To avoid these issues, avoid "
+ "using distutils directly, ensure that setuptools is installed in the "
+ "traditional way (e.g. not an editable install), and/or make sure "
+ "that setuptools is always imported before distutils.")
+
+
+def clear_distutils():
+ if 'distutils' not in sys.modules:
+ return
+ warnings.warn("Setuptools is replacing distutils.")
+ mods = [name for name in sys.modules if re.match(r'distutils\b', name)]
+ for name in mods:
+ del sys.modules[name]
+
+
+def enabled():
+ """
+ Allow selection of distutils by environment variable.
+ """
+ which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'stdlib')
+ return which == 'local'
+
+
+def ensure_local_distutils():
+ clear_distutils()
+ distutils = importlib.import_module('setuptools._distutils')
+ distutils.__name__ = 'distutils'
+ sys.modules['distutils'] = distutils
+
+ # sanity check that submodules load as expected
+ core = importlib.import_module('distutils.core')
+ assert '_distutils' in core.__file__, core.__file__
+
+
+def do_override():
+ """
+ Ensure that the local copy of distutils is preferred over stdlib.
+
+ See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
+ for more motivation.
+ """
+ if enabled():
+ warn_distutils_present()
+ ensure_local_distutils()
+
+
+class DistutilsMetaFinder:
+ def find_spec(self, fullname, path, target=None):
+ if path is not None:
+ return
+
+ method_name = 'spec_for_{fullname}'.format(**locals())
+ method = getattr(self, method_name, lambda: None)
+ return method()
+
+ def spec_for_distutils(self):
+ import importlib.abc
+ import importlib.util
+
+ class DistutilsLoader(importlib.abc.Loader):
+
+ def create_module(self, spec):
+ return importlib.import_module('setuptools._distutils')
+
+ def exec_module(self, module):
+ pass
+
+ return importlib.util.spec_from_loader('distutils', DistutilsLoader())
+
+ def spec_for_pip(self):
+ """
+ Ensure stdlib distutils when running under pip.
+ See pypa/pip#8761 for rationale.
+ """
+ if self.pip_imported_during_build():
+ return
+ clear_distutils()
+ self.spec_for_distutils = lambda: None
+
+ @staticmethod
+ def pip_imported_during_build():
+ """
+ Detect if pip is being imported in a build script. Ref #2355.
+ """
+ import traceback
+ return any(
+ frame.f_globals['__file__'].endswith('setup.py')
+ for frame, line in traceback.walk_stack(None)
+ )
+
+
+DISTUTILS_FINDER = DistutilsMetaFinder()
+
+
+def add_shim():
+ sys.meta_path.insert(0, DISTUTILS_FINDER)
+
+
+def remove_shim():
+ try:
+ sys.meta_path.remove(DISTUTILS_FINDER)
+ except ValueError:
+ pass
diff --git a/backend/venv/lib/python3.9/site-packages/_distutils_hack/override.py b/backend/venv/lib/python3.9/site-packages/_distutils_hack/override.py
new file mode 100644
index 00000000..2cc433a4
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/_distutils_hack/override.py
@@ -0,0 +1 @@
+__import__('_distutils_hack').do_override()
diff --git a/backend/venv/lib/python3.9/site-packages/_yaml/__init__.py b/backend/venv/lib/python3.9/site-packages/_yaml/__init__.py
new file mode 100644
index 00000000..7baa8c4b
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/_yaml/__init__.py
@@ -0,0 +1,33 @@
+# This is a stub package designed to roughly emulate the _yaml
+# extension module, which previously existed as a standalone module
+# and has been moved into the `yaml` package namespace.
+# It does not perfectly mimic its old counterpart, but should get
+# close enough for anyone who's relying on it even when they shouldn't.
+import yaml
+
+# in some circumstances, the yaml module we imoprted may be from a different version, so we need
+# to tread carefully when poking at it here (it may not have the attributes we expect)
+if not getattr(yaml, '__with_libyaml__', False):
+ from sys import version_info
+
+ exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
+ raise exc("No module named '_yaml'")
+else:
+ from yaml._yaml import *
+ import warnings
+ warnings.warn(
+ 'The _yaml extension module is now located at yaml._yaml'
+ ' and its location is subject to change. To use the'
+ ' LibYAML-based parser and emitter, import from `yaml`:'
+ ' `from yaml import CLoader as Loader, CDumper as Dumper`.',
+ DeprecationWarning
+ )
+ del warnings
+ # Don't `del yaml` here because yaml is actually an existing
+ # namespace member of _yaml.
+
+__name__ = '_yaml'
+# If the module is top-level (i.e. not a part of any specific package)
+# then the attribute should be set to ''.
+# https://docs.python.org/3.8/library/types.html
+__package__ = ''
diff --git a/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/INSTALLER b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/INSTALLER
new file mode 100644
index 00000000..a1b589e3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/LICENSE b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/LICENSE
new file mode 100644
index 00000000..74b9ce34
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/LICENSE
@@ -0,0 +1,19 @@
+Copyright 2009-2023 Michael Bayer.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/METADATA b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/METADATA
new file mode 100644
index 00000000..7a6884d9
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/METADATA
@@ -0,0 +1,142 @@
+Metadata-Version: 2.1
+Name: alembic
+Version: 1.13.1
+Summary: A database migration tool for SQLAlchemy.
+Home-page: https://alembic.sqlalchemy.org
+Author: Mike Bayer
+Author-email: mike_mp@zzzcomputing.com
+License: MIT
+Project-URL: Documentation, https://alembic.sqlalchemy.org/en/latest/
+Project-URL: Changelog, https://alembic.sqlalchemy.org/en/latest/changelog.html
+Project-URL: Source, https://github.com/sqlalchemy/alembic/
+Project-URL: Issue Tracker, https://github.com/sqlalchemy/alembic/issues/
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Environment :: Console
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Database :: Front-Ends
+Requires-Python: >=3.8
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+Requires-Dist: SQLAlchemy >=1.3.0
+Requires-Dist: Mako
+Requires-Dist: typing-extensions >=4
+Requires-Dist: importlib-metadata ; python_version < "3.9"
+Requires-Dist: importlib-resources ; python_version < "3.9"
+Provides-Extra: tz
+Requires-Dist: backports.zoneinfo ; (python_version < "3.9") and extra == 'tz'
+
+Alembic is a database migrations tool written by the author
+of `SQLAlchemy `_. A migrations tool
+offers the following functionality:
+
+* Can emit ALTER statements to a database in order to change
+ the structure of tables and other constructs
+* Provides a system whereby "migration scripts" may be constructed;
+ each script indicates a particular series of steps that can "upgrade" a
+ target database to a new version, and optionally a series of steps that can
+ "downgrade" similarly, doing the same steps in reverse.
+* Allows the scripts to execute in some sequential manner.
+
+The goals of Alembic are:
+
+* Very open ended and transparent configuration and operation. A new
+ Alembic environment is generated from a set of templates which is selected
+ among a set of options when setup first occurs. The templates then deposit a
+ series of scripts that define fully how database connectivity is established
+ and how migration scripts are invoked; the migration scripts themselves are
+ generated from a template within that series of scripts. The scripts can
+ then be further customized to define exactly how databases will be
+ interacted with and what structure new migration files should take.
+* Full support for transactional DDL. The default scripts ensure that all
+ migrations occur within a transaction - for those databases which support
+ this (Postgresql, Microsoft SQL Server), migrations can be tested with no
+ need to manually undo changes upon failure.
+* Minimalist script construction. Basic operations like renaming
+ tables/columns, adding/removing columns, changing column attributes can be
+ performed through one line commands like alter_column(), rename_table(),
+ add_constraint(). There is no need to recreate full SQLAlchemy Table
+ structures for simple operations like these - the functions themselves
+ generate minimalist schema structures behind the scenes to achieve the given
+ DDL sequence.
+* "auto generation" of migrations. While real world migrations are far more
+ complex than what can be automatically determined, Alembic can still
+ eliminate the initial grunt work in generating new migration directives
+ from an altered schema. The ``--autogenerate`` feature will inspect the
+ current status of a database using SQLAlchemy's schema inspection
+ capabilities, compare it to the current state of the database model as
+ specified in Python, and generate a series of "candidate" migrations,
+ rendering them into a new migration script as Python directives. The
+ developer then edits the new file, adding additional directives and data
+ migrations as needed, to produce a finished migration. Table and column
+ level changes can be detected, with constraints and indexes to follow as
+ well.
+* Full support for migrations generated as SQL scripts. Those of us who
+ work in corporate environments know that direct access to DDL commands on a
+ production database is a rare privilege, and DBAs want textual SQL scripts.
+ Alembic's usage model and commands are oriented towards being able to run a
+ series of migrations into a textual output file as easily as it runs them
+ directly to a database. Care must be taken in this mode to not invoke other
+ operations that rely upon in-memory SELECTs of rows - Alembic tries to
+ provide helper constructs like bulk_insert() to help with data-oriented
+ operations that are compatible with script-based DDL.
+* Non-linear, dependency-graph versioning. Scripts are given UUID
+ identifiers similarly to a DVCS, and the linkage of one script to the next
+ is achieved via human-editable markers within the scripts themselves.
+ The structure of a set of migration files is considered as a
+ directed-acyclic graph, meaning any migration file can be dependent
+ on any other arbitrary set of migration files, or none at
+ all. Through this open-ended system, migration files can be organized
+ into branches, multiple roots, and mergepoints, without restriction.
+ Commands are provided to produce new branches, roots, and merges of
+ branches automatically.
+* Provide a library of ALTER constructs that can be used by any SQLAlchemy
+ application. The DDL constructs build upon SQLAlchemy's own DDLElement base
+ and can be used standalone by any application or script.
+* At long last, bring SQLite and its inability to ALTER things into the fold,
+ but in such a way that SQLite's very special workflow needs are accommodated
+ in an explicit way that makes the most of a bad situation, through the
+ concept of a "batch" migration, where multiple changes to a table can
+ be batched together to form a series of instructions for a single, subsequent
+ "move-and-copy" workflow. You can even use "move-and-copy" workflow for
+ other databases, if you want to recreate a table in the background
+ on a busy system.
+
+Documentation and status of Alembic is at https://alembic.sqlalchemy.org/
+
+The SQLAlchemy Project
+======================
+
+Alembic is part of the `SQLAlchemy Project `_ and
+adheres to the same standards and conventions as the core project.
+
+Development / Bug reporting / Pull requests
+___________________________________________
+
+Please refer to the
+`SQLAlchemy Community Guide `_ for
+guidelines on coding and participating in this project.
+
+Code of Conduct
+_______________
+
+Above all, SQLAlchemy places great emphasis on polite, thoughtful, and
+constructive communication between users and developers.
+Please see our current Code of Conduct at
+`Code of Conduct `_.
+
+License
+=======
+
+Alembic is distributed under the `MIT license
+`_.
diff --git a/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/RECORD b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/RECORD
new file mode 100644
index 00000000..2f1bc542
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/RECORD
@@ -0,0 +1,151 @@
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/__main__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/api.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/compare.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/render.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/rewriter.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/command.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/config.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/context.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/ddl/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/ddl/_autogen.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/ddl/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/ddl/impl.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/ddl/mssql.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/ddl/mysql.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/ddl/oracle.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/ddl/postgresql.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/ddl/sqlite.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/environment.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/migration.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/op.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/operations/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/operations/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/operations/batch.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/operations/ops.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/operations/schemaobj.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/operations/toimpl.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/runtime/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/runtime/environment.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/runtime/migration.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/script/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/script/base.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/script/revision.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/script/write_hooks.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/templates/async/env.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/templates/generic/env.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/templates/multidb/env.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/assertions.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/env.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/fixtures.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/plugin/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/plugin/bootstrap.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/requirements.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/schemacompare.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/suite/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/suite/_autogen_fixtures.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/suite/test_autogen_comments.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/suite/test_autogen_computed.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/suite/test_autogen_diffs.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/suite/test_autogen_fks.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/suite/test_autogen_identity.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/suite/test_environment.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/suite/test_op.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/util.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/testing/warnings.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/util/__init__.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/util/compat.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/util/editor.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/util/exc.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/util/langhelpers.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/util/messaging.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/util/pyfiles.cpython-39.pyc,,
+../../../../../../../../Library/Caches/com.apple.python/Users/wuzilong/work/00code/sqxp/backend/venv/lib/python3.9/site-packages/alembic/util/sqla_compat.cpython-39.pyc,,
+../../../bin/alembic,sha256=jlV-nJfHEHBW8Gu7ENtbybB1zrWGB9HrexsLiL7uI_A,202
+alembic-1.13.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+alembic-1.13.1.dist-info/LICENSE,sha256=soUmiob0QW6vTQWyrjiAwVb3xZqPk1pAK8BW6vszrwg,1058
+alembic-1.13.1.dist-info/METADATA,sha256=W1F2NBRkhqW55HiGmEIpdmiRt2skU5wwJd21UHFbSdQ,7390
+alembic-1.13.1.dist-info/RECORD,,
+alembic-1.13.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+alembic-1.13.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
+alembic-1.13.1.dist-info/entry_points.txt,sha256=aykM30soxwGN0pB7etLc1q0cHJbL9dy46RnK9VX4LLw,48
+alembic-1.13.1.dist-info/top_level.txt,sha256=FwKWd5VsPFC8iQjpu1u9Cn-JnK3-V1RhUCmWqz1cl-s,8
+alembic/__init__.py,sha256=PMiQT_1tHyC_Od3GDBArBk7r14v8F62_xkzliPq9tLU,63
+alembic/__main__.py,sha256=373m7-TBh72JqrSMYviGrxCHZo-cnweM8AGF8A22PmY,78
+alembic/autogenerate/__init__.py,sha256=ntmUTXhjLm4_zmqIwyVaECdpPDn6_u1yM9vYk6-553E,543
+alembic/autogenerate/api.py,sha256=Oc7MRtDhkSICsQ82fYP9bBMYaAjzzW2X_izM3AQU-OY,22171
+alembic/autogenerate/compare.py,sha256=3QLK2yCDW37bXbAIXcHGz4YOFlOW8bSfLHJ8bmzgG1M,44938
+alembic/autogenerate/render.py,sha256=uSbCpkh72mo00xGZ8CJa3teM_gqulCoNtxH0Ey8Ny1k,34939
+alembic/autogenerate/rewriter.py,sha256=uZWRkTYJoncoEJ5WY1QBRiozjyChqZDJPy4LtcRibjM,7846
+alembic/command.py,sha256=jWFNS-wPWA-Klfm0GsPfWh_8sPj4n7rKROJ0zrwhoR0,21712
+alembic/config.py,sha256=I12lm4V-AXSt-7nvub-Vtx5Zfa68pYP5xSrFQQd45rQ,22256
+alembic/context.py,sha256=hK1AJOQXJ29Bhn276GYcosxeG7pC5aZRT5E8c4bMJ4Q,195
+alembic/context.pyi,sha256=hUHbSnbSeEEMVkk0gDSXOq4_9edSjYzsjmmf-mL9Iao,31737
+alembic/ddl/__init__.py,sha256=Df8fy4Vn_abP8B7q3x8gyFwEwnLw6hs2Ljt_bV3EZWE,152
+alembic/ddl/_autogen.py,sha256=0no9ywWP8gjvO57Ozc2naab4qNusVNn2fiJekjc275g,9179
+alembic/ddl/base.py,sha256=Jd7oPoAOGjOMcdMUIzSKnTjd8NKnTd7IjBXXyVpDCkU,9955
+alembic/ddl/impl.py,sha256=vkhkXFpLPJBG9jW2kv_sR5CC5czNd1ERLjLtfLuOFP0,28778
+alembic/ddl/mssql.py,sha256=ydvgBSaftKYjaBaMyqius66Ta4CICQSj79Og3Ed2atY,14219
+alembic/ddl/mysql.py,sha256=am221U_UK3wX33tNcXNiOObZV-Pa4CXuv7vN-epF9IU,16788
+alembic/ddl/oracle.py,sha256=TmoCq_FlbfyWAAk3e_q6mMQU0YmlfIcgKHpRfNMmgr0,6211
+alembic/ddl/postgresql.py,sha256=dcWLdDSqivzizVCce_H6RnOVAayPXDFfns-NC4-UaA8,29842
+alembic/ddl/sqlite.py,sha256=wLXhb8bJWRspKQTb-iVfepR4LXYgOuEbUWKX5qwDhIQ,7570
+alembic/environment.py,sha256=MM5lPayGT04H3aeng1H7GQ8HEAs3VGX5yy6mDLCPLT4,43
+alembic/migration.py,sha256=MV6Fju6rZtn2fTREKzXrCZM6aIBGII4OMZFix0X-GLs,41
+alembic/op.py,sha256=flHtcsVqOD-ZgZKK2pv-CJ5Cwh-KJ7puMUNXzishxLw,167
+alembic/op.pyi,sha256=8R6SJr1dQharU0blmMJJj23XFO_hk9ZmAQBJBQOeXRU,49816
+alembic/operations/__init__.py,sha256=e0KQSZAgLpTWvyvreB7DWg7RJV_MWSOPVDgCqsd2FzY,318
+alembic/operations/base.py,sha256=LCx4NH5NA2NLWQFaZTqE_p2KgLtqJ76oVcp1Grj-zFM,74004
+alembic/operations/batch.py,sha256=YqtD4hJ3_RkFxvI7zbmBwxcLEyLHYyWQpsz4l5L85yI,26943
+alembic/operations/ops.py,sha256=2vtYFhYFDEVq158HwORfGTsobDM7c-t0lewuR7JKw7g,94353
+alembic/operations/schemaobj.py,sha256=vjoD57QvjbzzA-jyPIXulbOmb5_bGPtxoq58KsKI4Y0,9424
+alembic/operations/toimpl.py,sha256=SoNY2_gZX2baXTD-pNjpCWnON8D2QBSYQBIjo13-WKA,7115
+alembic/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+alembic/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+alembic/runtime/environment.py,sha256=9wSJaePNAXBXvirif_85ql7dSq4bXM1E6pSb2k-6uGI,41508
+alembic/runtime/migration.py,sha256=Yfv2fa11wiQ0WgoZaFldlWxCPq4dVDOCEOxST_-1VB0,50066
+alembic/script/__init__.py,sha256=lSj06O391Iy5avWAiq8SPs6N8RBgxkSPjP8wpXcNDGg,100
+alembic/script/base.py,sha256=4gkppn2FKCYDoBgzGkTslcwdyxSabmHvGq0uGKulwbI,37586
+alembic/script/revision.py,sha256=sfnXQw2UwiXs0E6gEPHBKWuSsB5KyuxZPTrFn__hIEk,62060
+alembic/script/write_hooks.py,sha256=NGB6NGgfdf7HK6XNNpSKqUCfzxazj-NRUePgFx7MJSM,5036
+alembic/templates/async/README,sha256=ISVtAOvqvKk_5ThM5ioJE-lMkvf9IbknFUFVU_vPma4,58
+alembic/templates/async/alembic.ini.mako,sha256=uuhJETLWQuiYcs_jAOXHEjshEJ7VslEc1q4RRj0HWbE,3525
+alembic/templates/async/env.py,sha256=zbOCf3Y7w2lg92hxSwmG1MM_7y56i_oRH4AKp0pQBYo,2389
+alembic/templates/async/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
+alembic/templates/generic/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
+alembic/templates/generic/alembic.ini.mako,sha256=sT7F852yN3c8X1-GKFlhuWExXxw9hY1eb1ZZ9flFSzc,3634
+alembic/templates/generic/env.py,sha256=TLRWOVW3Xpt_Tpf8JFzlnoPn_qoUu8UV77Y4o9XD6yI,2103
+alembic/templates/generic/script.py.mako,sha256=MEqL-2qATlST9TAOeYgscMn1uy6HUS9NFvDgl93dMj8,635
+alembic/templates/multidb/README,sha256=dWLDhnBgphA4Nzb7sNlMfCS3_06YqVbHhz-9O5JNqyI,606
+alembic/templates/multidb/alembic.ini.mako,sha256=mPh8JFJfWiGs6tMtL8_HAQ-Dz1QOoJgE5Vm76nIMqgU,3728
+alembic/templates/multidb/env.py,sha256=6zNjnW8mXGUk7erTsAvrfhvqoczJ-gagjVq1Ypg2YIQ,4230
+alembic/templates/multidb/script.py.mako,sha256=N06nMtNSwHkgl0EBXDyMt8njp9tlOesR583gfq21nbY,1090
+alembic/testing/__init__.py,sha256=kOxOh5nwmui9d-_CCq9WA4Udwy7ITjm453w74CTLZDo,1159
+alembic/testing/assertions.py,sha256=1CbJk8c8-WO9eJ0XJ0jJvMsNRLUrXV41NOeIJUAlOBk,5015
+alembic/testing/env.py,sha256=zJacVb_z6uLs2U1TtkmnFH9P3_F-3IfYbVv4UEPOvfo,10754
+alembic/testing/fixtures.py,sha256=NyP4wE_dFN9ZzSGiBagRu1cdzkka03nwJYJYHYrrkSY,9112
+alembic/testing/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+alembic/testing/plugin/bootstrap.py,sha256=9C6wtjGrIVztZ928w27hsQE0KcjDLIUtUN3dvZKsMVk,50
+alembic/testing/requirements.py,sha256=dKeAO1l5TwBqXarJN-IPORlCqCJv-41Dj6oXoEikxHQ,5133
+alembic/testing/schemacompare.py,sha256=N5UqSNCOJetIKC4vKhpYzQEpj08XkdgIoqBmEPQ3tlc,4838
+alembic/testing/suite/__init__.py,sha256=MvE7-hwbaVN1q3NM-ztGxORU9dnIelUCINKqNxewn7Y,288
+alembic/testing/suite/_autogen_fixtures.py,sha256=cDq1pmzHe15S6dZPGNC6sqFaCQ3hLT_oPV2IDigUGQ0,9880
+alembic/testing/suite/test_autogen_comments.py,sha256=aEGqKUDw4kHjnDk298aoGcQvXJWmZXcIX_2FxH4cJK8,6283
+alembic/testing/suite/test_autogen_computed.py,sha256=qJeBpc8urnwTFvbwWrSTIbHVkRUuCXP-dKaNbUK2U2U,6077
+alembic/testing/suite/test_autogen_diffs.py,sha256=T4SR1n_kmcOKYhR4W1-dA0e5sddJ69DSVL2HW96kAkE,8394
+alembic/testing/suite/test_autogen_fks.py,sha256=AqFmb26Buex167HYa9dZWOk8x-JlB1OK3bwcvvjDFaU,32927
+alembic/testing/suite/test_autogen_identity.py,sha256=kcuqngG7qXAKPJDX4U8sRzPKHEJECHuZ0DtuaS6tVkk,5824
+alembic/testing/suite/test_environment.py,sha256=w9F0xnLEbALeR8k6_-Tz6JHvy91IqiTSypNasVzXfZQ,11877
+alembic/testing/suite/test_op.py,sha256=2XQCdm_NmnPxHGuGj7hmxMzIhKxXNotUsKdACXzE1mM,1343
+alembic/testing/util.py,sha256=CQrcQDA8fs_7ME85z5ydb-Bt70soIIID-qNY1vbR2dg,3350
+alembic/testing/warnings.py,sha256=RxA7x_8GseANgw07Us8JN_1iGbANxaw6_VitX2ZGQH4,1078
+alembic/util/__init__.py,sha256=KSZ7UT2YzH6CietgUtljVoE3QnGjoFKOi7RL5sgUxrk,1688
+alembic/util/compat.py,sha256=RjHdQa1NomU3Zlvgfvza0OMiSRQSLRL3xVl3OdUy2UE,2594
+alembic/util/editor.py,sha256=JIz6_BdgV8_oKtnheR6DZoB7qnrHrlRgWjx09AsTsUw,2546
+alembic/util/exc.py,sha256=KQTru4zcgAmN4IxLMwLFS56XToUewaXB7oOLcPNjPwg,98
+alembic/util/langhelpers.py,sha256=KYyOjFjJ26evPmrwhdTvLQNXN0bK7AIy5sRdKD91Fvg,10038
+alembic/util/messaging.py,sha256=BM5OCZ6qmLftFRw5yPSxj539_QmfVwNYoU8qYsDqoJY,3132
+alembic/util/pyfiles.py,sha256=zltVdcwEJJCPS2gHsQvkHkQakuF6wXiZ6zfwHbGNT0g,3489
+alembic/util/sqla_compat.py,sha256=toD1S63PgZ6iEteP9bwIf5E7DIUdQPo0UQ_Fn18qWnI,19536
diff --git a/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/REQUESTED b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/REQUESTED
new file mode 100644
index 00000000..e69de29b
diff --git a/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/WHEEL b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/WHEEL
new file mode 100644
index 00000000..98c0d20b
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.42.0)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/entry_points.txt b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/entry_points.txt
new file mode 100644
index 00000000..59452681
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/entry_points.txt
@@ -0,0 +1,2 @@
+[console_scripts]
+alembic = alembic.config:main
diff --git a/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/top_level.txt b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/top_level.txt
new file mode 100644
index 00000000..b5bd98d3
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic-1.13.1.dist-info/top_level.txt
@@ -0,0 +1 @@
+alembic
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/__init__.py b/backend/venv/lib/python3.9/site-packages/alembic/__init__.py
new file mode 100644
index 00000000..c153c8aa
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/__init__.py
@@ -0,0 +1,4 @@
+from . import context
+from . import op
+
+__version__ = "1.13.1"
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/__main__.py b/backend/venv/lib/python3.9/site-packages/alembic/__main__.py
new file mode 100644
index 00000000..af1b8e87
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/__main__.py
@@ -0,0 +1,4 @@
+from .config import main
+
+if __name__ == "__main__":
+ main(prog="alembic")
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/__init__.py b/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/__init__.py
new file mode 100644
index 00000000..445ddb25
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/__init__.py
@@ -0,0 +1,10 @@
+from .api import _render_migration_diffs as _render_migration_diffs
+from .api import compare_metadata as compare_metadata
+from .api import produce_migrations as produce_migrations
+from .api import render_python_code as render_python_code
+from .api import RevisionContext as RevisionContext
+from .compare import _produce_net_changes as _produce_net_changes
+from .compare import comparators as comparators
+from .render import render_op_text as render_op_text
+from .render import renderers as renderers
+from .rewriter import Rewriter as Rewriter
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/api.py b/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/api.py
new file mode 100644
index 00000000..aa8f32f6
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/api.py
@@ -0,0 +1,650 @@
+from __future__ import annotations
+
+import contextlib
+from typing import Any
+from typing import Dict
+from typing import Iterator
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import inspect
+
+from . import compare
+from . import render
+from .. import util
+from ..operations import ops
+from ..util import sqla_compat
+
+"""Provide the 'autogenerate' feature which can produce migration operations
+automatically."""
+
+if TYPE_CHECKING:
+ from sqlalchemy.engine import Connection
+ from sqlalchemy.engine import Dialect
+ from sqlalchemy.engine import Inspector
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.sql.schema import Table
+
+ from ..config import Config
+ from ..operations.ops import DowngradeOps
+ from ..operations.ops import MigrationScript
+ from ..operations.ops import UpgradeOps
+ from ..runtime.environment import NameFilterParentNames
+ from ..runtime.environment import NameFilterType
+ from ..runtime.environment import ProcessRevisionDirectiveFn
+ from ..runtime.environment import RenderItemFn
+ from ..runtime.migration import MigrationContext
+ from ..script.base import Script
+ from ..script.base import ScriptDirectory
+ from ..script.revision import _GetRevArg
+
+
+def compare_metadata(context: MigrationContext, metadata: MetaData) -> Any:
+ """Compare a database schema to that given in a
+ :class:`~sqlalchemy.schema.MetaData` instance.
+
+ The database connection is presented in the context
+ of a :class:`.MigrationContext` object, which
+ provides database connectivity as well as optional
+ comparison functions to use for datatypes and
+ server defaults - see the "autogenerate" arguments
+ at :meth:`.EnvironmentContext.configure`
+ for details on these.
+
+ The return format is a list of "diff" directives,
+ each representing individual differences::
+
+ from alembic.migration import MigrationContext
+ from alembic.autogenerate import compare_metadata
+ from sqlalchemy import (
+ create_engine,
+ MetaData,
+ Column,
+ Integer,
+ String,
+ Table,
+ text,
+ )
+ import pprint
+
+ engine = create_engine("sqlite://")
+
+ with engine.begin() as conn:
+ conn.execute(
+ text(
+ '''
+ create table foo (
+ id integer not null primary key,
+ old_data varchar,
+ x integer
+ )
+ '''
+ )
+ )
+ conn.execute(text("create table bar (data varchar)"))
+
+ metadata = MetaData()
+ Table(
+ "foo",
+ metadata,
+ Column("id", Integer, primary_key=True),
+ Column("data", Integer),
+ Column("x", Integer, nullable=False),
+ )
+ Table("bat", metadata, Column("info", String))
+
+ mc = MigrationContext.configure(engine.connect())
+
+ diff = compare_metadata(mc, metadata)
+ pprint.pprint(diff, indent=2, width=20)
+
+ Output::
+
+ [
+ (
+ "add_table",
+ Table(
+ "bat",
+ MetaData(),
+ Column("info", String(), table=),
+ schema=None,
+ ),
+ ),
+ (
+ "remove_table",
+ Table(
+ "bar",
+ MetaData(),
+ Column("data", VARCHAR(), table=),
+ schema=None,
+ ),
+ ),
+ (
+ "add_column",
+ None,
+ "foo",
+ Column("data", Integer(), table=),
+ ),
+ [
+ (
+ "modify_nullable",
+ None,
+ "foo",
+ "x",
+ {
+ "existing_comment": None,
+ "existing_server_default": False,
+ "existing_type": INTEGER(),
+ },
+ True,
+ False,
+ )
+ ],
+ (
+ "remove_column",
+ None,
+ "foo",
+ Column("old_data", VARCHAR(), table=),
+ ),
+ ]
+
+ :param context: a :class:`.MigrationContext`
+ instance.
+ :param metadata: a :class:`~sqlalchemy.schema.MetaData`
+ instance.
+
+ .. seealso::
+
+ :func:`.produce_migrations` - produces a :class:`.MigrationScript`
+ structure based on metadata comparison.
+
+ """
+
+ migration_script = produce_migrations(context, metadata)
+ assert migration_script.upgrade_ops is not None
+ return migration_script.upgrade_ops.as_diffs()
+
+
+def produce_migrations(
+ context: MigrationContext, metadata: MetaData
+) -> MigrationScript:
+ """Produce a :class:`.MigrationScript` structure based on schema
+ comparison.
+
+ This function does essentially what :func:`.compare_metadata` does,
+ but then runs the resulting list of diffs to produce the full
+ :class:`.MigrationScript` object. For an example of what this looks like,
+ see the example in :ref:`customizing_revision`.
+
+ .. seealso::
+
+ :func:`.compare_metadata` - returns more fundamental "diff"
+ data from comparing a schema.
+
+ """
+
+ autogen_context = AutogenContext(context, metadata=metadata)
+
+ migration_script = ops.MigrationScript(
+ rev_id=None,
+ upgrade_ops=ops.UpgradeOps([]),
+ downgrade_ops=ops.DowngradeOps([]),
+ )
+
+ compare._populate_migration_script(autogen_context, migration_script)
+
+ return migration_script
+
+
+def render_python_code(
+ up_or_down_op: Union[UpgradeOps, DowngradeOps],
+ sqlalchemy_module_prefix: str = "sa.",
+ alembic_module_prefix: str = "op.",
+ render_as_batch: bool = False,
+ imports: Sequence[str] = (),
+ render_item: Optional[RenderItemFn] = None,
+ migration_context: Optional[MigrationContext] = None,
+ user_module_prefix: Optional[str] = None,
+) -> str:
+ """Render Python code given an :class:`.UpgradeOps` or
+ :class:`.DowngradeOps` object.
+
+ This is a convenience function that can be used to test the
+ autogenerate output of a user-defined :class:`.MigrationScript` structure.
+
+ :param up_or_down_op: :class:`.UpgradeOps` or :class:`.DowngradeOps` object
+ :param sqlalchemy_module_prefix: module prefix for SQLAlchemy objects
+ :param alembic_module_prefix: module prefix for Alembic constructs
+ :param render_as_batch: use "batch operations" style for rendering
+ :param imports: sequence of import symbols to add
+ :param render_item: callable to render items
+ :param migration_context: optional :class:`.MigrationContext`
+ :param user_module_prefix: optional string prefix for user-defined types
+
+ .. versionadded:: 1.11.0
+
+ """
+ opts = {
+ "sqlalchemy_module_prefix": sqlalchemy_module_prefix,
+ "alembic_module_prefix": alembic_module_prefix,
+ "render_item": render_item,
+ "render_as_batch": render_as_batch,
+ "user_module_prefix": user_module_prefix,
+ }
+
+ if migration_context is None:
+ from ..runtime.migration import MigrationContext
+ from sqlalchemy.engine.default import DefaultDialect
+
+ migration_context = MigrationContext.configure(
+ dialect=DefaultDialect()
+ )
+
+ autogen_context = AutogenContext(migration_context, opts=opts)
+ autogen_context.imports = set(imports)
+ return render._indent(
+ render._render_cmd_body(up_or_down_op, autogen_context)
+ )
+
+
+def _render_migration_diffs(
+ context: MigrationContext, template_args: Dict[Any, Any]
+) -> None:
+ """legacy, used by test_autogen_composition at the moment"""
+
+ autogen_context = AutogenContext(context)
+
+ upgrade_ops = ops.UpgradeOps([])
+ compare._produce_net_changes(autogen_context, upgrade_ops)
+
+ migration_script = ops.MigrationScript(
+ rev_id=None,
+ upgrade_ops=upgrade_ops,
+ downgrade_ops=upgrade_ops.reverse(),
+ )
+
+ render._render_python_into_templatevars(
+ autogen_context, migration_script, template_args
+ )
+
+
+class AutogenContext:
+ """Maintains configuration and state that's specific to an
+ autogenerate operation."""
+
+ metadata: Optional[MetaData] = None
+ """The :class:`~sqlalchemy.schema.MetaData` object
+ representing the destination.
+
+ This object is the one that is passed within ``env.py``
+ to the :paramref:`.EnvironmentContext.configure.target_metadata`
+ parameter. It represents the structure of :class:`.Table` and other
+ objects as stated in the current database model, and represents the
+ destination structure for the database being examined.
+
+ While the :class:`~sqlalchemy.schema.MetaData` object is primarily
+ known as a collection of :class:`~sqlalchemy.schema.Table` objects,
+ it also has an :attr:`~sqlalchemy.schema.MetaData.info` dictionary
+ that may be used by end-user schemes to store additional schema-level
+ objects that are to be compared in custom autogeneration schemes.
+
+ """
+
+ connection: Optional[Connection] = None
+ """The :class:`~sqlalchemy.engine.base.Connection` object currently
+ connected to the database backend being compared.
+
+ This is obtained from the :attr:`.MigrationContext.bind` and is
+ ultimately set up in the ``env.py`` script.
+
+ """
+
+ dialect: Optional[Dialect] = None
+ """The :class:`~sqlalchemy.engine.Dialect` object currently in use.
+
+ This is normally obtained from the
+ :attr:`~sqlalchemy.engine.base.Connection.dialect` attribute.
+
+ """
+
+ imports: Set[str] = None # type: ignore[assignment]
+ """A ``set()`` which contains string Python import directives.
+
+ The directives are to be rendered into the ``${imports}`` section
+ of a script template. The set is normally empty and can be modified
+ within hooks such as the
+ :paramref:`.EnvironmentContext.configure.render_item` hook.
+
+ .. seealso::
+
+ :ref:`autogen_render_types`
+
+ """
+
+ migration_context: MigrationContext = None # type: ignore[assignment]
+ """The :class:`.MigrationContext` established by the ``env.py`` script."""
+
+ def __init__(
+ self,
+ migration_context: MigrationContext,
+ metadata: Optional[MetaData] = None,
+ opts: Optional[Dict[str, Any]] = None,
+ autogenerate: bool = True,
+ ) -> None:
+ if (
+ autogenerate
+ and migration_context is not None
+ and migration_context.as_sql
+ ):
+ raise util.CommandError(
+ "autogenerate can't use as_sql=True as it prevents querying "
+ "the database for schema information"
+ )
+
+ if opts is None:
+ opts = migration_context.opts
+
+ self.metadata = metadata = (
+ opts.get("target_metadata", None) if metadata is None else metadata
+ )
+
+ if (
+ autogenerate
+ and metadata is None
+ and migration_context is not None
+ and migration_context.script is not None
+ ):
+ raise util.CommandError(
+ "Can't proceed with --autogenerate option; environment "
+ "script %s does not provide "
+ "a MetaData object or sequence of objects to the context."
+ % (migration_context.script.env_py_location)
+ )
+
+ include_object = opts.get("include_object", None)
+ include_name = opts.get("include_name", None)
+
+ object_filters = []
+ name_filters = []
+ if include_object:
+ object_filters.append(include_object)
+ if include_name:
+ name_filters.append(include_name)
+
+ self._object_filters = object_filters
+ self._name_filters = name_filters
+
+ self.migration_context = migration_context
+ if self.migration_context is not None:
+ self.connection = self.migration_context.bind
+ self.dialect = self.migration_context.dialect
+
+ self.imports = set()
+ self.opts: Dict[str, Any] = opts
+ self._has_batch: bool = False
+
+ @util.memoized_property
+ def inspector(self) -> Inspector:
+ if self.connection is None:
+ raise TypeError(
+ "can't return inspector as this "
+ "AutogenContext has no database connection"
+ )
+ return inspect(self.connection)
+
+ @contextlib.contextmanager
+ def _within_batch(self) -> Iterator[None]:
+ self._has_batch = True
+ yield
+ self._has_batch = False
+
+ def run_name_filters(
+ self,
+ name: Optional[str],
+ type_: NameFilterType,
+ parent_names: NameFilterParentNames,
+ ) -> bool:
+ """Run the context's name filters and return True if the targets
+ should be part of the autogenerate operation.
+
+ This method should be run for every kind of name encountered within the
+ reflection side of an autogenerate operation, giving the environment
+ the chance to filter what names should be reflected as database
+ objects. The filters here are produced directly via the
+ :paramref:`.EnvironmentContext.configure.include_name` parameter.
+
+ """
+ if "schema_name" in parent_names:
+ if type_ == "table":
+ table_name = name
+ else:
+ table_name = parent_names.get("table_name", None)
+ if table_name:
+ schema_name = parent_names["schema_name"]
+ if schema_name:
+ parent_names["schema_qualified_table_name"] = "%s.%s" % (
+ schema_name,
+ table_name,
+ )
+ else:
+ parent_names["schema_qualified_table_name"] = table_name
+
+ for fn in self._name_filters:
+ if not fn(name, type_, parent_names):
+ return False
+ else:
+ return True
+
+ def run_object_filters(
+ self,
+ object_: SchemaItem,
+ name: sqla_compat._ConstraintName,
+ type_: NameFilterType,
+ reflected: bool,
+ compare_to: Optional[SchemaItem],
+ ) -> bool:
+ """Run the context's object filters and return True if the targets
+ should be part of the autogenerate operation.
+
+ This method should be run for every kind of object encountered within
+ an autogenerate operation, giving the environment the chance
+ to filter what objects should be included in the comparison.
+ The filters here are produced directly via the
+ :paramref:`.EnvironmentContext.configure.include_object` parameter.
+
+ """
+ for fn in self._object_filters:
+ if not fn(object_, name, type_, reflected, compare_to):
+ return False
+ else:
+ return True
+
+ run_filters = run_object_filters
+
+ @util.memoized_property
+ def sorted_tables(self) -> List[Table]:
+ """Return an aggregate of the :attr:`.MetaData.sorted_tables`
+ collection(s).
+
+ For a sequence of :class:`.MetaData` objects, this
+ concatenates the :attr:`.MetaData.sorted_tables` collection
+ for each individual :class:`.MetaData` in the order of the
+ sequence. It does **not** collate the sorted tables collections.
+
+ """
+ result = []
+ for m in util.to_list(self.metadata):
+ result.extend(m.sorted_tables)
+ return result
+
+ @util.memoized_property
+ def table_key_to_table(self) -> Dict[str, Table]:
+ """Return an aggregate of the :attr:`.MetaData.tables` dictionaries.
+
+ The :attr:`.MetaData.tables` collection is a dictionary of table key
+ to :class:`.Table`; this method aggregates the dictionary across
+ multiple :class:`.MetaData` objects into one dictionary.
+
+ Duplicate table keys are **not** supported; if two :class:`.MetaData`
+ objects contain the same table key, an exception is raised.
+
+ """
+ result: Dict[str, Table] = {}
+ for m in util.to_list(self.metadata):
+ intersect = set(result).intersection(set(m.tables))
+ if intersect:
+ raise ValueError(
+ "Duplicate table keys across multiple "
+ "MetaData objects: %s"
+ % (", ".join('"%s"' % key for key in sorted(intersect)))
+ )
+
+ result.update(m.tables)
+ return result
+
+
+class RevisionContext:
+ """Maintains configuration and state that's specific to a revision
+ file generation operation."""
+
+ generated_revisions: List[MigrationScript]
+ process_revision_directives: Optional[ProcessRevisionDirectiveFn]
+
+ def __init__(
+ self,
+ config: Config,
+ script_directory: ScriptDirectory,
+ command_args: Dict[str, Any],
+ process_revision_directives: Optional[
+ ProcessRevisionDirectiveFn
+ ] = None,
+ ) -> None:
+ self.config = config
+ self.script_directory = script_directory
+ self.command_args = command_args
+ self.process_revision_directives = process_revision_directives
+ self.template_args = {
+ "config": config # Let templates use config for
+ # e.g. multiple databases
+ }
+ self.generated_revisions = [self._default_revision()]
+
+ def _to_script(
+ self, migration_script: MigrationScript
+ ) -> Optional[Script]:
+ template_args: Dict[str, Any] = self.template_args.copy()
+
+ if getattr(migration_script, "_needs_render", False):
+ autogen_context = self._last_autogen_context
+
+ # clear out existing imports if we are doing multiple
+ # renders
+ autogen_context.imports = set()
+ if migration_script.imports:
+ autogen_context.imports.update(migration_script.imports)
+ render._render_python_into_templatevars(
+ autogen_context, migration_script, template_args
+ )
+
+ assert migration_script.rev_id is not None
+ return self.script_directory.generate_revision(
+ migration_script.rev_id,
+ migration_script.message,
+ refresh=True,
+ head=migration_script.head,
+ splice=migration_script.splice,
+ branch_labels=migration_script.branch_label,
+ version_path=migration_script.version_path,
+ depends_on=migration_script.depends_on,
+ **template_args,
+ )
+
+ def run_autogenerate(
+ self, rev: _GetRevArg, migration_context: MigrationContext
+ ) -> None:
+ self._run_environment(rev, migration_context, True)
+
+ def run_no_autogenerate(
+ self, rev: _GetRevArg, migration_context: MigrationContext
+ ) -> None:
+ self._run_environment(rev, migration_context, False)
+
+ def _run_environment(
+ self,
+ rev: _GetRevArg,
+ migration_context: MigrationContext,
+ autogenerate: bool,
+ ) -> None:
+ if autogenerate:
+ if self.command_args["sql"]:
+ raise util.CommandError(
+ "Using --sql with --autogenerate does not make any sense"
+ )
+ if set(self.script_directory.get_revisions(rev)) != set(
+ self.script_directory.get_revisions("heads")
+ ):
+ raise util.CommandError("Target database is not up to date.")
+
+ upgrade_token = migration_context.opts["upgrade_token"]
+ downgrade_token = migration_context.opts["downgrade_token"]
+
+ migration_script = self.generated_revisions[-1]
+ if not getattr(migration_script, "_needs_render", False):
+ migration_script.upgrade_ops_list[-1].upgrade_token = upgrade_token
+ migration_script.downgrade_ops_list[
+ -1
+ ].downgrade_token = downgrade_token
+ migration_script._needs_render = True
+ else:
+ migration_script._upgrade_ops.append(
+ ops.UpgradeOps([], upgrade_token=upgrade_token)
+ )
+ migration_script._downgrade_ops.append(
+ ops.DowngradeOps([], downgrade_token=downgrade_token)
+ )
+
+ autogen_context = AutogenContext(
+ migration_context, autogenerate=autogenerate
+ )
+ self._last_autogen_context: AutogenContext = autogen_context
+
+ if autogenerate:
+ compare._populate_migration_script(
+ autogen_context, migration_script
+ )
+
+ if self.process_revision_directives:
+ self.process_revision_directives(
+ migration_context, rev, self.generated_revisions
+ )
+
+ hook = migration_context.opts["process_revision_directives"]
+ if hook:
+ hook(migration_context, rev, self.generated_revisions)
+
+ for migration_script in self.generated_revisions:
+ migration_script._needs_render = True
+
+ def _default_revision(self) -> MigrationScript:
+ command_args: Dict[str, Any] = self.command_args
+ op = ops.MigrationScript(
+ rev_id=command_args["rev_id"] or util.rev_id(),
+ message=command_args["message"],
+ upgrade_ops=ops.UpgradeOps([]),
+ downgrade_ops=ops.DowngradeOps([]),
+ head=command_args["head"],
+ splice=command_args["splice"],
+ branch_label=command_args["branch_label"],
+ version_path=command_args["version_path"],
+ depends_on=command_args["depends_on"],
+ )
+ return op
+
+ def generate_scripts(self) -> Iterator[Optional[Script]]:
+ for generated_revision in self.generated_revisions:
+ yield self._to_script(generated_revision)
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/compare.py b/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/compare.py
new file mode 100644
index 00000000..fcef531a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/compare.py
@@ -0,0 +1,1329 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import contextlib
+import logging
+import re
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Iterator
+from typing import Mapping
+from typing import Optional
+from typing import Set
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from sqlalchemy import event
+from sqlalchemy import inspect
+from sqlalchemy import schema as sa_schema
+from sqlalchemy import text
+from sqlalchemy import types as sqltypes
+from sqlalchemy.sql import expression
+from sqlalchemy.sql.schema import ForeignKeyConstraint
+from sqlalchemy.sql.schema import Index
+from sqlalchemy.sql.schema import UniqueConstraint
+from sqlalchemy.util import OrderedSet
+
+from .. import util
+from ..ddl._autogen import is_index_sig
+from ..ddl._autogen import is_uq_sig
+from ..operations import ops
+from ..util import sqla_compat
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy.engine.reflection import Inspector
+ from sqlalchemy.sql.elements import quoted_name
+ from sqlalchemy.sql.elements import TextClause
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Table
+
+ from alembic.autogenerate.api import AutogenContext
+ from alembic.ddl.impl import DefaultImpl
+ from alembic.operations.ops import AlterColumnOp
+ from alembic.operations.ops import MigrationScript
+ from alembic.operations.ops import ModifyTableOps
+ from alembic.operations.ops import UpgradeOps
+ from ..ddl._autogen import _constraint_sig
+
+
+log = logging.getLogger(__name__)
+
+
+def _populate_migration_script(
+ autogen_context: AutogenContext, migration_script: MigrationScript
+) -> None:
+ upgrade_ops = migration_script.upgrade_ops_list[-1]
+ downgrade_ops = migration_script.downgrade_ops_list[-1]
+
+ _produce_net_changes(autogen_context, upgrade_ops)
+ upgrade_ops.reverse_into(downgrade_ops)
+
+
+comparators = util.Dispatcher(uselist=True)
+
+
+def _produce_net_changes(
+ autogen_context: AutogenContext, upgrade_ops: UpgradeOps
+) -> None:
+ connection = autogen_context.connection
+ assert connection is not None
+ include_schemas = autogen_context.opts.get("include_schemas", False)
+
+ inspector: Inspector = inspect(connection)
+
+ default_schema = connection.dialect.default_schema_name
+ schemas: Set[Optional[str]]
+ if include_schemas:
+ schemas = set(inspector.get_schema_names())
+ # replace default schema name with None
+ schemas.discard("information_schema")
+ # replace the "default" schema with None
+ schemas.discard(default_schema)
+ schemas.add(None)
+ else:
+ schemas = {None}
+
+ schemas = {
+ s for s in schemas if autogen_context.run_name_filters(s, "schema", {})
+ }
+
+ assert autogen_context.dialect is not None
+ comparators.dispatch("schema", autogen_context.dialect.name)(
+ autogen_context, upgrade_ops, schemas
+ )
+
+
+@comparators.dispatch_for("schema")
+def _autogen_for_tables(
+ autogen_context: AutogenContext,
+ upgrade_ops: UpgradeOps,
+ schemas: Union[Set[None], Set[Optional[str]]],
+) -> None:
+ inspector = autogen_context.inspector
+
+ conn_table_names: Set[Tuple[Optional[str], str]] = set()
+
+ version_table_schema = (
+ autogen_context.migration_context.version_table_schema
+ )
+ version_table = autogen_context.migration_context.version_table
+
+ for schema_name in schemas:
+ tables = set(inspector.get_table_names(schema=schema_name))
+ if schema_name == version_table_schema:
+ tables = tables.difference(
+ [autogen_context.migration_context.version_table]
+ )
+
+ conn_table_names.update(
+ (schema_name, tname)
+ for tname in tables
+ if autogen_context.run_name_filters(
+ tname, "table", {"schema_name": schema_name}
+ )
+ )
+
+ metadata_table_names = OrderedSet(
+ [(table.schema, table.name) for table in autogen_context.sorted_tables]
+ ).difference([(version_table_schema, version_table)])
+
+ _compare_tables(
+ conn_table_names,
+ metadata_table_names,
+ inspector,
+ upgrade_ops,
+ autogen_context,
+ )
+
+
+def _compare_tables(
+ conn_table_names: set,
+ metadata_table_names: set,
+ inspector: Inspector,
+ upgrade_ops: UpgradeOps,
+ autogen_context: AutogenContext,
+) -> None:
+ default_schema = inspector.bind.dialect.default_schema_name
+
+ # tables coming from the connection will not have "schema"
+ # set if it matches default_schema_name; so we need a list
+ # of table names from local metadata that also have "None" if schema
+ # == default_schema_name. Most setups will be like this anyway but
+ # some are not (see #170)
+ metadata_table_names_no_dflt_schema = OrderedSet(
+ [
+ (schema if schema != default_schema else None, tname)
+ for schema, tname in metadata_table_names
+ ]
+ )
+
+ # to adjust for the MetaData collection storing the tables either
+ # as "schemaname.tablename" or just "tablename", create a new lookup
+ # which will match the "non-default-schema" keys to the Table object.
+ tname_to_table = {
+ no_dflt_schema: autogen_context.table_key_to_table[
+ sa_schema._get_table_key(tname, schema)
+ ]
+ for no_dflt_schema, (schema, tname) in zip(
+ metadata_table_names_no_dflt_schema, metadata_table_names
+ )
+ }
+ metadata_table_names = metadata_table_names_no_dflt_schema
+
+ for s, tname in metadata_table_names.difference(conn_table_names):
+ name = "%s.%s" % (s, tname) if s else tname
+ metadata_table = tname_to_table[(s, tname)]
+ if autogen_context.run_object_filters(
+ metadata_table, tname, "table", False, None
+ ):
+ upgrade_ops.ops.append(
+ ops.CreateTableOp.from_table(metadata_table)
+ )
+ log.info("Detected added table %r", name)
+ modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
+
+ comparators.dispatch("table")(
+ autogen_context,
+ modify_table_ops,
+ s,
+ tname,
+ None,
+ metadata_table,
+ )
+ if not modify_table_ops.is_empty():
+ upgrade_ops.ops.append(modify_table_ops)
+
+ removal_metadata = sa_schema.MetaData()
+ for s, tname in conn_table_names.difference(metadata_table_names):
+ name = sa_schema._get_table_key(tname, s)
+ exists = name in removal_metadata.tables
+ t = sa_schema.Table(tname, removal_metadata, schema=s)
+
+ if not exists:
+ event.listen(
+ t,
+ "column_reflect",
+ # fmt: off
+ autogen_context.migration_context.impl.
+ _compat_autogen_column_reflect
+ (inspector),
+ # fmt: on
+ )
+ sqla_compat._reflect_table(inspector, t)
+ if autogen_context.run_object_filters(t, tname, "table", True, None):
+ modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
+
+ comparators.dispatch("table")(
+ autogen_context, modify_table_ops, s, tname, t, None
+ )
+ if not modify_table_ops.is_empty():
+ upgrade_ops.ops.append(modify_table_ops)
+
+ upgrade_ops.ops.append(ops.DropTableOp.from_table(t))
+ log.info("Detected removed table %r", name)
+
+ existing_tables = conn_table_names.intersection(metadata_table_names)
+
+ existing_metadata = sa_schema.MetaData()
+ conn_column_info = {}
+ for s, tname in existing_tables:
+ name = sa_schema._get_table_key(tname, s)
+ exists = name in existing_metadata.tables
+ t = sa_schema.Table(tname, existing_metadata, schema=s)
+ if not exists:
+ event.listen(
+ t,
+ "column_reflect",
+ # fmt: off
+ autogen_context.migration_context.impl.
+ _compat_autogen_column_reflect(inspector),
+ # fmt: on
+ )
+ sqla_compat._reflect_table(inspector, t)
+ conn_column_info[(s, tname)] = t
+
+ for s, tname in sorted(existing_tables, key=lambda x: (x[0] or "", x[1])):
+ s = s or None
+ name = "%s.%s" % (s, tname) if s else tname
+ metadata_table = tname_to_table[(s, tname)]
+ conn_table = existing_metadata.tables[name]
+
+ if autogen_context.run_object_filters(
+ metadata_table, tname, "table", False, conn_table
+ ):
+ modify_table_ops = ops.ModifyTableOps(tname, [], schema=s)
+ with _compare_columns(
+ s,
+ tname,
+ conn_table,
+ metadata_table,
+ modify_table_ops,
+ autogen_context,
+ inspector,
+ ):
+ comparators.dispatch("table")(
+ autogen_context,
+ modify_table_ops,
+ s,
+ tname,
+ conn_table,
+ metadata_table,
+ )
+
+ if not modify_table_ops.is_empty():
+ upgrade_ops.ops.append(modify_table_ops)
+
+
+_IndexColumnSortingOps: Mapping[str, Any] = util.immutabledict(
+ {
+ "asc": expression.asc,
+ "desc": expression.desc,
+ "nulls_first": expression.nullsfirst,
+ "nulls_last": expression.nullslast,
+ "nullsfirst": expression.nullsfirst, # 1_3 name
+ "nullslast": expression.nullslast, # 1_3 name
+ }
+)
+
+
+def _make_index(
+ impl: DefaultImpl, params: Dict[str, Any], conn_table: Table
+) -> Optional[Index]:
+ exprs: list[Union[Column[Any], TextClause]] = []
+ sorting = params.get("column_sorting")
+
+ for num, col_name in enumerate(params["column_names"]):
+ item: Union[Column[Any], TextClause]
+ if col_name is None:
+ assert "expressions" in params
+ name = params["expressions"][num]
+ item = text(name)
+ else:
+ name = col_name
+ item = conn_table.c[col_name]
+ if sorting and name in sorting:
+ for operator in sorting[name]:
+ if operator in _IndexColumnSortingOps:
+ item = _IndexColumnSortingOps[operator](item)
+ exprs.append(item)
+ ix = sa_schema.Index(
+ params["name"],
+ *exprs,
+ unique=params["unique"],
+ _table=conn_table,
+ **impl.adjust_reflected_dialect_options(params, "index"),
+ )
+ if "duplicates_constraint" in params:
+ ix.info["duplicates_constraint"] = params["duplicates_constraint"]
+ return ix
+
+
+def _make_unique_constraint(
+ impl: DefaultImpl, params: Dict[str, Any], conn_table: Table
+) -> UniqueConstraint:
+ uq = sa_schema.UniqueConstraint(
+ *[conn_table.c[cname] for cname in params["column_names"]],
+ name=params["name"],
+ **impl.adjust_reflected_dialect_options(params, "unique_constraint"),
+ )
+ if "duplicates_index" in params:
+ uq.info["duplicates_index"] = params["duplicates_index"]
+
+ return uq
+
+
+def _make_foreign_key(
+ params: Dict[str, Any], conn_table: Table
+) -> ForeignKeyConstraint:
+ tname = params["referred_table"]
+ if params["referred_schema"]:
+ tname = "%s.%s" % (params["referred_schema"], tname)
+
+ options = params.get("options", {})
+
+ const = sa_schema.ForeignKeyConstraint(
+ [conn_table.c[cname] for cname in params["constrained_columns"]],
+ ["%s.%s" % (tname, n) for n in params["referred_columns"]],
+ onupdate=options.get("onupdate"),
+ ondelete=options.get("ondelete"),
+ deferrable=options.get("deferrable"),
+ initially=options.get("initially"),
+ name=params["name"],
+ )
+ # needed by 0.7
+ conn_table.append_constraint(const)
+ return const
+
+
+@contextlib.contextmanager
+def _compare_columns(
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ conn_table: Table,
+ metadata_table: Table,
+ modify_table_ops: ModifyTableOps,
+ autogen_context: AutogenContext,
+ inspector: Inspector,
+) -> Iterator[None]:
+ name = "%s.%s" % (schema, tname) if schema else tname
+ metadata_col_names = OrderedSet(
+ c.name for c in metadata_table.c if not c.system
+ )
+ metadata_cols_by_name = {
+ c.name: c for c in metadata_table.c if not c.system
+ }
+
+ conn_col_names = {
+ c.name: c
+ for c in conn_table.c
+ if autogen_context.run_name_filters(
+ c.name, "column", {"table_name": tname, "schema_name": schema}
+ )
+ }
+
+ for cname in metadata_col_names.difference(conn_col_names):
+ if autogen_context.run_object_filters(
+ metadata_cols_by_name[cname], cname, "column", False, None
+ ):
+ modify_table_ops.ops.append(
+ ops.AddColumnOp.from_column_and_tablename(
+ schema, tname, metadata_cols_by_name[cname]
+ )
+ )
+ log.info("Detected added column '%s.%s'", name, cname)
+
+ for colname in metadata_col_names.intersection(conn_col_names):
+ metadata_col = metadata_cols_by_name[colname]
+ conn_col = conn_table.c[colname]
+ if not autogen_context.run_object_filters(
+ metadata_col, colname, "column", False, conn_col
+ ):
+ continue
+ alter_column_op = ops.AlterColumnOp(tname, colname, schema=schema)
+
+ comparators.dispatch("column")(
+ autogen_context,
+ alter_column_op,
+ schema,
+ tname,
+ colname,
+ conn_col,
+ metadata_col,
+ )
+
+ if alter_column_op.has_changes():
+ modify_table_ops.ops.append(alter_column_op)
+
+ yield
+
+ for cname in set(conn_col_names).difference(metadata_col_names):
+ if autogen_context.run_object_filters(
+ conn_table.c[cname], cname, "column", True, None
+ ):
+ modify_table_ops.ops.append(
+ ops.DropColumnOp.from_column_and_tablename(
+ schema, tname, conn_table.c[cname]
+ )
+ )
+ log.info("Detected removed column '%s.%s'", name, cname)
+
+
+_C = TypeVar("_C", bound=Union[UniqueConstraint, ForeignKeyConstraint, Index])
+
+
+@comparators.dispatch_for("table")
+def _compare_indexes_and_uniques(
+ autogen_context: AutogenContext,
+ modify_ops: ModifyTableOps,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ conn_table: Optional[Table],
+ metadata_table: Optional[Table],
+) -> None:
+ inspector = autogen_context.inspector
+ is_create_table = conn_table is None
+ is_drop_table = metadata_table is None
+ impl = autogen_context.migration_context.impl
+
+ # 1a. get raw indexes and unique constraints from metadata ...
+ if metadata_table is not None:
+ metadata_unique_constraints = {
+ uq
+ for uq in metadata_table.constraints
+ if isinstance(uq, sa_schema.UniqueConstraint)
+ }
+ metadata_indexes = set(metadata_table.indexes)
+ else:
+ metadata_unique_constraints = set()
+ metadata_indexes = set()
+
+ conn_uniques = conn_indexes = frozenset() # type:ignore[var-annotated]
+
+ supports_unique_constraints = False
+
+ unique_constraints_duplicate_unique_indexes = False
+
+ if conn_table is not None:
+ # 1b. ... and from connection, if the table exists
+ try:
+ conn_uniques = inspector.get_unique_constraints( # type:ignore[assignment] # noqa
+ tname, schema=schema
+ )
+ supports_unique_constraints = True
+ except NotImplementedError:
+ pass
+ except TypeError:
+ # number of arguments is off for the base
+ # method in SQLAlchemy due to the cache decorator
+ # not being present
+ pass
+ else:
+ conn_uniques = [ # type:ignore[assignment]
+ uq
+ for uq in conn_uniques
+ if autogen_context.run_name_filters(
+ uq["name"],
+ "unique_constraint",
+ {"table_name": tname, "schema_name": schema},
+ )
+ ]
+ for uq in conn_uniques:
+ if uq.get("duplicates_index"):
+ unique_constraints_duplicate_unique_indexes = True
+ try:
+ conn_indexes = inspector.get_indexes( # type:ignore[assignment]
+ tname, schema=schema
+ )
+ except NotImplementedError:
+ pass
+ else:
+ conn_indexes = [ # type:ignore[assignment]
+ ix
+ for ix in conn_indexes
+ if autogen_context.run_name_filters(
+ ix["name"],
+ "index",
+ {"table_name": tname, "schema_name": schema},
+ )
+ ]
+
+ # 2. convert conn-level objects from raw inspector records
+ # into schema objects
+ if is_drop_table:
+ # for DROP TABLE uniques are inline, don't need them
+ conn_uniques = set() # type:ignore[assignment]
+ else:
+ conn_uniques = { # type:ignore[assignment]
+ _make_unique_constraint(impl, uq_def, conn_table)
+ for uq_def in conn_uniques
+ }
+
+ conn_indexes = { # type:ignore[assignment]
+ index
+ for index in (
+ _make_index(impl, ix, conn_table) for ix in conn_indexes
+ )
+ if index is not None
+ }
+
+ # 2a. if the dialect dupes unique indexes as unique constraints
+ # (mysql and oracle), correct for that
+
+ if unique_constraints_duplicate_unique_indexes:
+ _correct_for_uq_duplicates_uix(
+ conn_uniques,
+ conn_indexes,
+ metadata_unique_constraints,
+ metadata_indexes,
+ autogen_context.dialect,
+ impl,
+ )
+
+ # 3. give the dialect a chance to omit indexes and constraints that
+ # we know are either added implicitly by the DB or that the DB
+ # can't accurately report on
+ impl.correct_for_autogen_constraints(
+ conn_uniques, # type: ignore[arg-type]
+ conn_indexes, # type: ignore[arg-type]
+ metadata_unique_constraints,
+ metadata_indexes,
+ )
+
+ # 4. organize the constraints into "signature" collections, the
+ # _constraint_sig() objects provide a consistent facade over both
+ # Index and UniqueConstraint so we can easily work with them
+ # interchangeably
+ metadata_unique_constraints_sig = {
+ impl._create_metadata_constraint_sig(uq)
+ for uq in metadata_unique_constraints
+ }
+
+ metadata_indexes_sig = {
+ impl._create_metadata_constraint_sig(ix) for ix in metadata_indexes
+ }
+
+ conn_unique_constraints = {
+ impl._create_reflected_constraint_sig(uq) for uq in conn_uniques
+ }
+
+ conn_indexes_sig = {
+ impl._create_reflected_constraint_sig(ix) for ix in conn_indexes
+ }
+
+ # 5. index things by name, for those objects that have names
+ metadata_names = {
+ cast(str, c.md_name_to_sql_name(autogen_context)): c
+ for c in metadata_unique_constraints_sig.union(metadata_indexes_sig)
+ if c.is_named
+ }
+
+ conn_uniques_by_name: Dict[sqla_compat._ConstraintName, _constraint_sig]
+ conn_indexes_by_name: Dict[sqla_compat._ConstraintName, _constraint_sig]
+
+ conn_uniques_by_name = {c.name: c for c in conn_unique_constraints}
+ conn_indexes_by_name = {c.name: c for c in conn_indexes_sig}
+ conn_names = {
+ c.name: c
+ for c in conn_unique_constraints.union(conn_indexes_sig)
+ if sqla_compat.constraint_name_string(c.name)
+ }
+
+ doubled_constraints = {
+ name: (conn_uniques_by_name[name], conn_indexes_by_name[name])
+ for name in set(conn_uniques_by_name).intersection(
+ conn_indexes_by_name
+ )
+ }
+
+ # 6. index things by "column signature", to help with unnamed unique
+ # constraints.
+ conn_uniques_by_sig = {uq.unnamed: uq for uq in conn_unique_constraints}
+ metadata_uniques_by_sig = {
+ uq.unnamed: uq for uq in metadata_unique_constraints_sig
+ }
+ unnamed_metadata_uniques = {
+ uq.unnamed: uq
+ for uq in metadata_unique_constraints_sig
+ if not sqla_compat._constraint_is_named(
+ uq.const, autogen_context.dialect
+ )
+ }
+
+ # assumptions:
+ # 1. a unique constraint or an index from the connection *always*
+ # has a name.
+ # 2. an index on the metadata side *always* has a name.
+ # 3. a unique constraint on the metadata side *might* have a name.
+ # 4. The backend may double up indexes as unique constraints and
+ # vice versa (e.g. MySQL, Postgresql)
+
+ def obj_added(obj: _constraint_sig):
+ if is_index_sig(obj):
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "index", False, None
+ ):
+ modify_ops.ops.append(ops.CreateIndexOp.from_index(obj.const))
+ log.info(
+ "Detected added index '%r' on '%s'",
+ obj.name,
+ obj.column_names,
+ )
+ elif is_uq_sig(obj):
+ if not supports_unique_constraints:
+ # can't report unique indexes as added if we don't
+ # detect them
+ return
+ if is_create_table or is_drop_table:
+ # unique constraints are created inline with table defs
+ return
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "unique_constraint", False, None
+ ):
+ modify_ops.ops.append(
+ ops.AddConstraintOp.from_constraint(obj.const)
+ )
+ log.info(
+ "Detected added unique constraint %r on '%s'",
+ obj.name,
+ obj.column_names,
+ )
+ else:
+ assert False
+
+ def obj_removed(obj: _constraint_sig):
+ if is_index_sig(obj):
+ if obj.is_unique and not supports_unique_constraints:
+ # many databases double up unique constraints
+ # as unique indexes. without that list we can't
+ # be sure what we're doing here
+ return
+
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "index", True, None
+ ):
+ modify_ops.ops.append(ops.DropIndexOp.from_index(obj.const))
+ log.info("Detected removed index %r on %r", obj.name, tname)
+ elif is_uq_sig(obj):
+ if is_create_table or is_drop_table:
+ # if the whole table is being dropped, we don't need to
+ # consider unique constraint separately
+ return
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "unique_constraint", True, None
+ ):
+ modify_ops.ops.append(
+ ops.DropConstraintOp.from_constraint(obj.const)
+ )
+ log.info(
+ "Detected removed unique constraint %r on %r",
+ obj.name,
+ tname,
+ )
+ else:
+ assert False
+
+ def obj_changed(
+ old: _constraint_sig,
+ new: _constraint_sig,
+ msg: str,
+ ):
+ if is_index_sig(old):
+ assert is_index_sig(new)
+
+ if autogen_context.run_object_filters(
+ new.const, new.name, "index", False, old.const
+ ):
+ log.info(
+ "Detected changed index %r on %r: %s", old.name, tname, msg
+ )
+ modify_ops.ops.append(ops.DropIndexOp.from_index(old.const))
+ modify_ops.ops.append(ops.CreateIndexOp.from_index(new.const))
+ elif is_uq_sig(old):
+ assert is_uq_sig(new)
+
+ if autogen_context.run_object_filters(
+ new.const, new.name, "unique_constraint", False, old.const
+ ):
+ log.info(
+ "Detected changed unique constraint %r on %r: %s",
+ old.name,
+ tname,
+ msg,
+ )
+ modify_ops.ops.append(
+ ops.DropConstraintOp.from_constraint(old.const)
+ )
+ modify_ops.ops.append(
+ ops.AddConstraintOp.from_constraint(new.const)
+ )
+ else:
+ assert False
+
+ for removed_name in sorted(set(conn_names).difference(metadata_names)):
+ conn_obj = conn_names[removed_name]
+ if (
+ is_uq_sig(conn_obj)
+ and conn_obj.unnamed in unnamed_metadata_uniques
+ ):
+ continue
+ elif removed_name in doubled_constraints:
+ conn_uq, conn_idx = doubled_constraints[removed_name]
+ if (
+ all(
+ conn_idx.unnamed != meta_idx.unnamed
+ for meta_idx in metadata_indexes_sig
+ )
+ and conn_uq.unnamed not in metadata_uniques_by_sig
+ ):
+ obj_removed(conn_uq)
+ obj_removed(conn_idx)
+ else:
+ obj_removed(conn_obj)
+
+ for existing_name in sorted(set(metadata_names).intersection(conn_names)):
+ metadata_obj = metadata_names[existing_name]
+
+ if existing_name in doubled_constraints:
+ conn_uq, conn_idx = doubled_constraints[existing_name]
+ if is_index_sig(metadata_obj):
+ conn_obj = conn_idx
+ else:
+ conn_obj = conn_uq
+ else:
+ conn_obj = conn_names[existing_name]
+
+ if type(conn_obj) != type(metadata_obj):
+ obj_removed(conn_obj)
+ obj_added(metadata_obj)
+ else:
+ comparison = metadata_obj.compare_to_reflected(conn_obj)
+
+ if comparison.is_different:
+ # constraint are different
+ obj_changed(conn_obj, metadata_obj, comparison.message)
+ elif comparison.is_skip:
+ # constraint cannot be compared, skip them
+ thing = (
+ "index" if is_index_sig(conn_obj) else "unique constraint"
+ )
+ log.info(
+ "Cannot compare %s %r, assuming equal and skipping. %s",
+ thing,
+ conn_obj.name,
+ comparison.message,
+ )
+ else:
+ # constraint are equal
+ assert comparison.is_equal
+
+ for added_name in sorted(set(metadata_names).difference(conn_names)):
+ obj = metadata_names[added_name]
+ obj_added(obj)
+
+ for uq_sig in unnamed_metadata_uniques:
+ if uq_sig not in conn_uniques_by_sig:
+ obj_added(unnamed_metadata_uniques[uq_sig])
+
+
+def _correct_for_uq_duplicates_uix(
+ conn_unique_constraints,
+ conn_indexes,
+ metadata_unique_constraints,
+ metadata_indexes,
+ dialect,
+ impl,
+):
+ # dedupe unique indexes vs. constraints, since MySQL / Oracle
+ # doesn't really have unique constraints as a separate construct.
+ # but look in the metadata and try to maintain constructs
+ # that already seem to be defined one way or the other
+ # on that side. This logic was formerly local to MySQL dialect,
+ # generalized to Oracle and others. See #276
+
+ # resolve final rendered name for unique constraints defined in the
+ # metadata. this includes truncation of long names. naming convention
+ # names currently should already be set as cons.name, however leave this
+ # to the sqla_compat to decide.
+ metadata_cons_names = [
+ (sqla_compat._get_constraint_final_name(cons, dialect), cons)
+ for cons in metadata_unique_constraints
+ ]
+
+ metadata_uq_names = {
+ name for name, cons in metadata_cons_names if name is not None
+ }
+
+ unnamed_metadata_uqs = {
+ impl._create_metadata_constraint_sig(cons).unnamed
+ for name, cons in metadata_cons_names
+ if name is None
+ }
+
+ metadata_ix_names = {
+ sqla_compat._get_constraint_final_name(cons, dialect)
+ for cons in metadata_indexes
+ if cons.unique
+ }
+
+ # for reflection side, names are in their final database form
+ # already since they're from the database
+ conn_ix_names = {cons.name: cons for cons in conn_indexes if cons.unique}
+
+ uqs_dupe_indexes = {
+ cons.name: cons
+ for cons in conn_unique_constraints
+ if cons.info["duplicates_index"]
+ }
+
+ for overlap in uqs_dupe_indexes:
+ if overlap not in metadata_uq_names:
+ if (
+ impl._create_reflected_constraint_sig(
+ uqs_dupe_indexes[overlap]
+ ).unnamed
+ not in unnamed_metadata_uqs
+ ):
+ conn_unique_constraints.discard(uqs_dupe_indexes[overlap])
+ elif overlap not in metadata_ix_names:
+ conn_indexes.discard(conn_ix_names[overlap])
+
+
+@comparators.dispatch_for("column")
+def _compare_nullable(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ cname: Union[quoted_name, str],
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> None:
+ metadata_col_nullable = metadata_col.nullable
+ conn_col_nullable = conn_col.nullable
+ alter_column_op.existing_nullable = conn_col_nullable
+
+ if conn_col_nullable is not metadata_col_nullable:
+ if (
+ sqla_compat._server_default_is_computed(
+ metadata_col.server_default, conn_col.server_default
+ )
+ and sqla_compat._nullability_might_be_unset(metadata_col)
+ or (
+ sqla_compat._server_default_is_identity(
+ metadata_col.server_default, conn_col.server_default
+ )
+ )
+ ):
+ log.info(
+ "Ignoring nullable change on identity column '%s.%s'",
+ tname,
+ cname,
+ )
+ else:
+ alter_column_op.modify_nullable = metadata_col_nullable
+ log.info(
+ "Detected %s on column '%s.%s'",
+ "NULL" if metadata_col_nullable else "NOT NULL",
+ tname,
+ cname,
+ )
+
+
+@comparators.dispatch_for("column")
+def _setup_autoincrement(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ cname: quoted_name,
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> None:
+ if metadata_col.table._autoincrement_column is metadata_col:
+ alter_column_op.kw["autoincrement"] = True
+ elif metadata_col.autoincrement is True:
+ alter_column_op.kw["autoincrement"] = True
+ elif metadata_col.autoincrement is False:
+ alter_column_op.kw["autoincrement"] = False
+
+
+@comparators.dispatch_for("column")
+def _compare_type(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ cname: Union[quoted_name, str],
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> None:
+ conn_type = conn_col.type
+ alter_column_op.existing_type = conn_type
+ metadata_type = metadata_col.type
+ if conn_type._type_affinity is sqltypes.NullType:
+ log.info(
+ "Couldn't determine database type " "for column '%s.%s'",
+ tname,
+ cname,
+ )
+ return
+ if metadata_type._type_affinity is sqltypes.NullType:
+ log.info(
+ "Column '%s.%s' has no type within " "the model; can't compare",
+ tname,
+ cname,
+ )
+ return
+
+ isdiff = autogen_context.migration_context._compare_type(
+ conn_col, metadata_col
+ )
+
+ if isdiff:
+ alter_column_op.modify_type = metadata_type
+ log.info(
+ "Detected type change from %r to %r on '%s.%s'",
+ conn_type,
+ metadata_type,
+ tname,
+ cname,
+ )
+
+
+def _render_server_default_for_compare(
+ metadata_default: Optional[Any], autogen_context: AutogenContext
+) -> Optional[str]:
+ if isinstance(metadata_default, sa_schema.DefaultClause):
+ if isinstance(metadata_default.arg, str):
+ metadata_default = metadata_default.arg
+ else:
+ metadata_default = str(
+ metadata_default.arg.compile(
+ dialect=autogen_context.dialect,
+ compile_kwargs={"literal_binds": True},
+ )
+ )
+ if isinstance(metadata_default, str):
+ return metadata_default
+ else:
+ return None
+
+
+def _normalize_computed_default(sqltext: str) -> str:
+ """we want to warn if a computed sql expression has changed. however
+ we don't want false positives and the warning is not that critical.
+ so filter out most forms of variability from the SQL text.
+
+ """
+
+ return re.sub(r"[ \(\)'\"`\[\]]", "", sqltext).lower()
+
+
+def _compare_computed_default(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: str,
+ cname: str,
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> None:
+ rendered_metadata_default = str(
+ cast(sa_schema.Computed, metadata_col.server_default).sqltext.compile(
+ dialect=autogen_context.dialect,
+ compile_kwargs={"literal_binds": True},
+ )
+ )
+
+ # since we cannot change computed columns, we do only a crude comparison
+ # here where we try to eliminate syntactical differences in order to
+ # get a minimal comparison just to emit a warning.
+
+ rendered_metadata_default = _normalize_computed_default(
+ rendered_metadata_default
+ )
+
+ if isinstance(conn_col.server_default, sa_schema.Computed):
+ rendered_conn_default = str(
+ conn_col.server_default.sqltext.compile(
+ dialect=autogen_context.dialect,
+ compile_kwargs={"literal_binds": True},
+ )
+ )
+ if rendered_conn_default is None:
+ rendered_conn_default = ""
+ else:
+ rendered_conn_default = _normalize_computed_default(
+ rendered_conn_default
+ )
+ else:
+ rendered_conn_default = ""
+
+ if rendered_metadata_default != rendered_conn_default:
+ _warn_computed_not_supported(tname, cname)
+
+
+def _warn_computed_not_supported(tname: str, cname: str) -> None:
+ util.warn("Computed default on %s.%s cannot be modified" % (tname, cname))
+
+
+def _compare_identity_default(
+ autogen_context,
+ alter_column_op,
+ schema,
+ tname,
+ cname,
+ conn_col,
+ metadata_col,
+):
+ impl = autogen_context.migration_context.impl
+ diff, ignored_attr, is_alter = impl._compare_identity_default(
+ metadata_col.server_default, conn_col.server_default
+ )
+
+ return diff, is_alter
+
+
+@comparators.dispatch_for("column")
+def _compare_server_default(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ cname: Union[quoted_name, str],
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> Optional[bool]:
+ metadata_default = metadata_col.server_default
+ conn_col_default = conn_col.server_default
+ if conn_col_default is None and metadata_default is None:
+ return False
+
+ if sqla_compat._server_default_is_computed(metadata_default):
+ # return False in case of a computed column as the server
+ # default. Note that DDL for adding or removing "GENERATED AS" from
+ # an existing column is not currently known for any backend.
+ # Once SQLAlchemy can reflect "GENERATED" as the "computed" element,
+ # we would also want to ignore and/or warn for changes vs. the
+ # metadata (or support backend specific DDL if applicable).
+ if not sqla_compat.has_computed_reflection:
+ return False
+
+ else:
+ return (
+ _compare_computed_default( # type:ignore[func-returns-value]
+ autogen_context,
+ alter_column_op,
+ schema,
+ tname,
+ cname,
+ conn_col,
+ metadata_col,
+ )
+ )
+ if sqla_compat._server_default_is_computed(conn_col_default):
+ _warn_computed_not_supported(tname, cname)
+ return False
+
+ if sqla_compat._server_default_is_identity(
+ metadata_default, conn_col_default
+ ):
+ alter_column_op.existing_server_default = conn_col_default
+ diff, is_alter = _compare_identity_default(
+ autogen_context,
+ alter_column_op,
+ schema,
+ tname,
+ cname,
+ conn_col,
+ metadata_col,
+ )
+ if is_alter:
+ alter_column_op.modify_server_default = metadata_default
+ if diff:
+ log.info(
+ "Detected server default on column '%s.%s': "
+ "identity options attributes %s",
+ tname,
+ cname,
+ sorted(diff),
+ )
+ else:
+ rendered_metadata_default = _render_server_default_for_compare(
+ metadata_default, autogen_context
+ )
+
+ rendered_conn_default = (
+ cast(Any, conn_col_default).arg.text if conn_col_default else None
+ )
+
+ alter_column_op.existing_server_default = conn_col_default
+
+ is_diff = autogen_context.migration_context._compare_server_default(
+ conn_col,
+ metadata_col,
+ rendered_metadata_default,
+ rendered_conn_default,
+ )
+ if is_diff:
+ alter_column_op.modify_server_default = metadata_default
+ log.info("Detected server default on column '%s.%s'", tname, cname)
+
+ return None
+
+
+@comparators.dispatch_for("column")
+def _compare_column_comment(
+ autogen_context: AutogenContext,
+ alter_column_op: AlterColumnOp,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ cname: quoted_name,
+ conn_col: Column[Any],
+ metadata_col: Column[Any],
+) -> Optional[Literal[False]]:
+ assert autogen_context.dialect is not None
+ if not autogen_context.dialect.supports_comments:
+ return None
+
+ metadata_comment = metadata_col.comment
+ conn_col_comment = conn_col.comment
+ if conn_col_comment is None and metadata_comment is None:
+ return False
+
+ alter_column_op.existing_comment = conn_col_comment
+
+ if conn_col_comment != metadata_comment:
+ alter_column_op.modify_comment = metadata_comment
+ log.info("Detected column comment '%s.%s'", tname, cname)
+
+ return None
+
+
+@comparators.dispatch_for("table")
+def _compare_foreign_keys(
+ autogen_context: AutogenContext,
+ modify_table_ops: ModifyTableOps,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ conn_table: Table,
+ metadata_table: Table,
+) -> None:
+ # if we're doing CREATE TABLE, all FKs are created
+ # inline within the table def
+ if conn_table is None or metadata_table is None:
+ return
+
+ inspector = autogen_context.inspector
+ metadata_fks = {
+ fk
+ for fk in metadata_table.constraints
+ if isinstance(fk, sa_schema.ForeignKeyConstraint)
+ }
+
+ conn_fks_list = [
+ fk
+ for fk in inspector.get_foreign_keys(tname, schema=schema)
+ if autogen_context.run_name_filters(
+ fk["name"],
+ "foreign_key_constraint",
+ {"table_name": tname, "schema_name": schema},
+ )
+ ]
+
+ conn_fks = {
+ _make_foreign_key(const, conn_table) # type: ignore[arg-type]
+ for const in conn_fks_list
+ }
+
+ impl = autogen_context.migration_context.impl
+
+ # give the dialect a chance to correct the FKs to match more
+ # closely
+ autogen_context.migration_context.impl.correct_for_autogen_foreignkeys(
+ conn_fks, metadata_fks
+ )
+
+ metadata_fks_sig = {
+ impl._create_metadata_constraint_sig(fk) for fk in metadata_fks
+ }
+
+ conn_fks_sig = {
+ impl._create_reflected_constraint_sig(fk) for fk in conn_fks
+ }
+
+ # check if reflected FKs include options, indicating the backend
+ # can reflect FK options
+ if conn_fks_list and "options" in conn_fks_list[0]:
+ conn_fks_by_sig = {c.unnamed: c for c in conn_fks_sig}
+ metadata_fks_by_sig = {c.unnamed: c for c in metadata_fks_sig}
+ else:
+ # otherwise compare by sig without options added
+ conn_fks_by_sig = {c.unnamed_no_options: c for c in conn_fks_sig}
+ metadata_fks_by_sig = {
+ c.unnamed_no_options: c for c in metadata_fks_sig
+ }
+
+ metadata_fks_by_name = {
+ c.name: c for c in metadata_fks_sig if c.name is not None
+ }
+ conn_fks_by_name = {c.name: c for c in conn_fks_sig if c.name is not None}
+
+ def _add_fk(obj, compare_to):
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "foreign_key_constraint", False, compare_to
+ ):
+ modify_table_ops.ops.append(
+ ops.CreateForeignKeyOp.from_constraint(const.const) # type: ignore[has-type] # noqa: E501
+ )
+
+ log.info(
+ "Detected added foreign key (%s)(%s) on table %s%s",
+ ", ".join(obj.source_columns),
+ ", ".join(obj.target_columns),
+ "%s." % obj.source_schema if obj.source_schema else "",
+ obj.source_table,
+ )
+
+ def _remove_fk(obj, compare_to):
+ if autogen_context.run_object_filters(
+ obj.const, obj.name, "foreign_key_constraint", True, compare_to
+ ):
+ modify_table_ops.ops.append(
+ ops.DropConstraintOp.from_constraint(obj.const)
+ )
+ log.info(
+ "Detected removed foreign key (%s)(%s) on table %s%s",
+ ", ".join(obj.source_columns),
+ ", ".join(obj.target_columns),
+ "%s." % obj.source_schema if obj.source_schema else "",
+ obj.source_table,
+ )
+
+ # so far it appears we don't need to do this by name at all.
+ # SQLite doesn't preserve constraint names anyway
+
+ for removed_sig in set(conn_fks_by_sig).difference(metadata_fks_by_sig):
+ const = conn_fks_by_sig[removed_sig]
+ if removed_sig not in metadata_fks_by_sig:
+ compare_to = (
+ metadata_fks_by_name[const.name].const
+ if const.name in metadata_fks_by_name
+ else None
+ )
+ _remove_fk(const, compare_to)
+
+ for added_sig in set(metadata_fks_by_sig).difference(conn_fks_by_sig):
+ const = metadata_fks_by_sig[added_sig]
+ if added_sig not in conn_fks_by_sig:
+ compare_to = (
+ conn_fks_by_name[const.name].const
+ if const.name in conn_fks_by_name
+ else None
+ )
+ _add_fk(const, compare_to)
+
+
+@comparators.dispatch_for("table")
+def _compare_table_comment(
+ autogen_context: AutogenContext,
+ modify_table_ops: ModifyTableOps,
+ schema: Optional[str],
+ tname: Union[quoted_name, str],
+ conn_table: Optional[Table],
+ metadata_table: Optional[Table],
+) -> None:
+ assert autogen_context.dialect is not None
+ if not autogen_context.dialect.supports_comments:
+ return
+
+ # if we're doing CREATE TABLE, comments will be created inline
+ # with the create_table op.
+ if conn_table is None or metadata_table is None:
+ return
+
+ if conn_table.comment is None and metadata_table.comment is None:
+ return
+
+ if metadata_table.comment is None and conn_table.comment is not None:
+ modify_table_ops.ops.append(
+ ops.DropTableCommentOp(
+ tname, existing_comment=conn_table.comment, schema=schema
+ )
+ )
+ elif metadata_table.comment != conn_table.comment:
+ modify_table_ops.ops.append(
+ ops.CreateTableCommentOp(
+ tname,
+ metadata_table.comment,
+ existing_comment=conn_table.comment,
+ schema=schema,
+ )
+ )
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/render.py b/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/render.py
new file mode 100644
index 00000000..317a6dbe
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/render.py
@@ -0,0 +1,1097 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+from io import StringIO
+import re
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from mako.pygen import PythonPrinter
+from sqlalchemy import schema as sa_schema
+from sqlalchemy import sql
+from sqlalchemy import types as sqltypes
+from sqlalchemy.sql.elements import conv
+from sqlalchemy.sql.elements import quoted_name
+
+from .. import util
+from ..operations import ops
+from ..util import sqla_compat
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy.sql.base import DialectKWArgs
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.elements import TextClause
+ from sqlalchemy.sql.schema import CheckConstraint
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.schema import FetchedValue
+ from sqlalchemy.sql.schema import ForeignKey
+ from sqlalchemy.sql.schema import ForeignKeyConstraint
+ from sqlalchemy.sql.schema import Index
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import PrimaryKeyConstraint
+ from sqlalchemy.sql.schema import UniqueConstraint
+ from sqlalchemy.sql.sqltypes import ARRAY
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from alembic.autogenerate.api import AutogenContext
+ from alembic.config import Config
+ from alembic.operations.ops import MigrationScript
+ from alembic.operations.ops import ModifyTableOps
+ from alembic.util.sqla_compat import Computed
+ from alembic.util.sqla_compat import Identity
+
+
+MAX_PYTHON_ARGS = 255
+
+
+def _render_gen_name(
+ autogen_context: AutogenContext,
+ name: sqla_compat._ConstraintName,
+) -> Optional[Union[quoted_name, str, _f_name]]:
+ if isinstance(name, conv):
+ return _f_name(_alembic_autogenerate_prefix(autogen_context), name)
+ else:
+ return sqla_compat.constraint_name_or_none(name)
+
+
+def _indent(text: str) -> str:
+ text = re.compile(r"^", re.M).sub(" ", text).strip()
+ text = re.compile(r" +$", re.M).sub("", text)
+ return text
+
+
+def _render_python_into_templatevars(
+ autogen_context: AutogenContext,
+ migration_script: MigrationScript,
+ template_args: Dict[str, Union[str, Config]],
+) -> None:
+ imports = autogen_context.imports
+
+ for upgrade_ops, downgrade_ops in zip(
+ migration_script.upgrade_ops_list, migration_script.downgrade_ops_list
+ ):
+ template_args[upgrade_ops.upgrade_token] = _indent(
+ _render_cmd_body(upgrade_ops, autogen_context)
+ )
+ template_args[downgrade_ops.downgrade_token] = _indent(
+ _render_cmd_body(downgrade_ops, autogen_context)
+ )
+ template_args["imports"] = "\n".join(sorted(imports))
+
+
+default_renderers = renderers = util.Dispatcher()
+
+
+def _render_cmd_body(
+ op_container: ops.OpContainer,
+ autogen_context: AutogenContext,
+) -> str:
+ buf = StringIO()
+ printer = PythonPrinter(buf)
+
+ printer.writeline(
+ "# ### commands auto generated by Alembic - please adjust! ###"
+ )
+
+ has_lines = False
+ for op in op_container.ops:
+ lines = render_op(autogen_context, op)
+ has_lines = has_lines or bool(lines)
+
+ for line in lines:
+ printer.writeline(line)
+
+ if not has_lines:
+ printer.writeline("pass")
+
+ printer.writeline("# ### end Alembic commands ###")
+
+ return buf.getvalue()
+
+
+def render_op(
+ autogen_context: AutogenContext, op: ops.MigrateOperation
+) -> List[str]:
+ renderer = renderers.dispatch(op)
+ lines = util.to_list(renderer(autogen_context, op))
+ return lines
+
+
+def render_op_text(
+ autogen_context: AutogenContext, op: ops.MigrateOperation
+) -> str:
+ return "\n".join(render_op(autogen_context, op))
+
+
+@renderers.dispatch_for(ops.ModifyTableOps)
+def _render_modify_table(
+ autogen_context: AutogenContext, op: ModifyTableOps
+) -> List[str]:
+ opts = autogen_context.opts
+ render_as_batch = opts.get("render_as_batch", False)
+
+ if op.ops:
+ lines = []
+ if render_as_batch:
+ with autogen_context._within_batch():
+ lines.append(
+ "with op.batch_alter_table(%r, schema=%r) as batch_op:"
+ % (op.table_name, op.schema)
+ )
+ for t_op in op.ops:
+ t_lines = render_op(autogen_context, t_op)
+ lines.extend(t_lines)
+ lines.append("")
+ else:
+ for t_op in op.ops:
+ t_lines = render_op(autogen_context, t_op)
+ lines.extend(t_lines)
+
+ return lines
+ else:
+ return []
+
+
+@renderers.dispatch_for(ops.CreateTableCommentOp)
+def _render_create_table_comment(
+ autogen_context: AutogenContext, op: ops.CreateTableCommentOp
+) -> str:
+ if autogen_context._has_batch:
+ templ = (
+ "{prefix}create_table_comment(\n"
+ "{indent}{comment},\n"
+ "{indent}existing_comment={existing}\n"
+ ")"
+ )
+ else:
+ templ = (
+ "{prefix}create_table_comment(\n"
+ "{indent}'{tname}',\n"
+ "{indent}{comment},\n"
+ "{indent}existing_comment={existing},\n"
+ "{indent}schema={schema}\n"
+ ")"
+ )
+ return templ.format(
+ prefix=_alembic_autogenerate_prefix(autogen_context),
+ tname=op.table_name,
+ comment="%r" % op.comment if op.comment is not None else None,
+ existing="%r" % op.existing_comment
+ if op.existing_comment is not None
+ else None,
+ schema="'%s'" % op.schema if op.schema is not None else None,
+ indent=" ",
+ )
+
+
+@renderers.dispatch_for(ops.DropTableCommentOp)
+def _render_drop_table_comment(
+ autogen_context: AutogenContext, op: ops.DropTableCommentOp
+) -> str:
+ if autogen_context._has_batch:
+ templ = (
+ "{prefix}drop_table_comment(\n"
+ "{indent}existing_comment={existing}\n"
+ ")"
+ )
+ else:
+ templ = (
+ "{prefix}drop_table_comment(\n"
+ "{indent}'{tname}',\n"
+ "{indent}existing_comment={existing},\n"
+ "{indent}schema={schema}\n"
+ ")"
+ )
+ return templ.format(
+ prefix=_alembic_autogenerate_prefix(autogen_context),
+ tname=op.table_name,
+ existing="%r" % op.existing_comment
+ if op.existing_comment is not None
+ else None,
+ schema="'%s'" % op.schema if op.schema is not None else None,
+ indent=" ",
+ )
+
+
+@renderers.dispatch_for(ops.CreateTableOp)
+def _add_table(autogen_context: AutogenContext, op: ops.CreateTableOp) -> str:
+ table = op.to_table()
+
+ args = [
+ col
+ for col in [
+ _render_column(col, autogen_context) for col in table.columns
+ ]
+ if col
+ ] + sorted(
+ [
+ rcons
+ for rcons in [
+ _render_constraint(
+ cons, autogen_context, op._namespace_metadata
+ )
+ for cons in table.constraints
+ ]
+ if rcons is not None
+ ]
+ )
+
+ if len(args) > MAX_PYTHON_ARGS:
+ args_str = "*[" + ",\n".join(args) + "]"
+ else:
+ args_str = ",\n".join(args)
+
+ text = "%(prefix)screate_table(%(tablename)r,\n%(args)s" % {
+ "tablename": _ident(op.table_name),
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "args": args_str,
+ }
+ if op.schema:
+ text += ",\nschema=%r" % _ident(op.schema)
+
+ comment = table.comment
+ if comment:
+ text += ",\ncomment=%r" % _ident(comment)
+
+ info = table.info
+ if info:
+ text += f",\ninfo={info!r}"
+
+ for k in sorted(op.kw):
+ text += ",\n%s=%r" % (k.replace(" ", "_"), op.kw[k])
+
+ if table._prefixes:
+ prefixes = ", ".join("'%s'" % p for p in table._prefixes)
+ text += ",\nprefixes=[%s]" % prefixes
+
+ text += "\n)"
+ return text
+
+
+@renderers.dispatch_for(ops.DropTableOp)
+def _drop_table(autogen_context: AutogenContext, op: ops.DropTableOp) -> str:
+ text = "%(prefix)sdrop_table(%(tname)r" % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "tname": _ident(op.table_name),
+ }
+ if op.schema:
+ text += ", schema=%r" % _ident(op.schema)
+ text += ")"
+ return text
+
+
+def _render_dialect_kwargs_items(
+ autogen_context: AutogenContext, item: DialectKWArgs
+) -> list[str]:
+ return [
+ f"{key}={_render_potential_expr(val, autogen_context)}"
+ for key, val in item.dialect_kwargs.items()
+ ]
+
+
+@renderers.dispatch_for(ops.CreateIndexOp)
+def _add_index(autogen_context: AutogenContext, op: ops.CreateIndexOp) -> str:
+ index = op.to_index()
+
+ has_batch = autogen_context._has_batch
+
+ if has_batch:
+ tmpl = (
+ "%(prefix)screate_index(%(name)r, [%(columns)s], "
+ "unique=%(unique)r%(kwargs)s)"
+ )
+ else:
+ tmpl = (
+ "%(prefix)screate_index(%(name)r, %(table)r, [%(columns)s], "
+ "unique=%(unique)r%(schema)s%(kwargs)s)"
+ )
+
+ assert index.table is not None
+
+ opts = _render_dialect_kwargs_items(autogen_context, index)
+ text = tmpl % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "name": _render_gen_name(autogen_context, index.name),
+ "table": _ident(index.table.name),
+ "columns": ", ".join(
+ _get_index_rendered_expressions(index, autogen_context)
+ ),
+ "unique": index.unique or False,
+ "schema": (", schema=%r" % _ident(index.table.schema))
+ if index.table.schema
+ else "",
+ "kwargs": ", " + ", ".join(opts) if opts else "",
+ }
+ return text
+
+
+@renderers.dispatch_for(ops.DropIndexOp)
+def _drop_index(autogen_context: AutogenContext, op: ops.DropIndexOp) -> str:
+ index = op.to_index()
+
+ has_batch = autogen_context._has_batch
+
+ if has_batch:
+ tmpl = "%(prefix)sdrop_index(%(name)r%(kwargs)s)"
+ else:
+ tmpl = (
+ "%(prefix)sdrop_index(%(name)r, "
+ "table_name=%(table_name)r%(schema)s%(kwargs)s)"
+ )
+ opts = _render_dialect_kwargs_items(autogen_context, index)
+ text = tmpl % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "name": _render_gen_name(autogen_context, op.index_name),
+ "table_name": _ident(op.table_name),
+ "schema": ((", schema=%r" % _ident(op.schema)) if op.schema else ""),
+ "kwargs": ", " + ", ".join(opts) if opts else "",
+ }
+ return text
+
+
+@renderers.dispatch_for(ops.CreateUniqueConstraintOp)
+def _add_unique_constraint(
+ autogen_context: AutogenContext, op: ops.CreateUniqueConstraintOp
+) -> List[str]:
+ return [_uq_constraint(op.to_constraint(), autogen_context, True)]
+
+
+@renderers.dispatch_for(ops.CreateForeignKeyOp)
+def _add_fk_constraint(
+ autogen_context: AutogenContext, op: ops.CreateForeignKeyOp
+) -> str:
+ args = [repr(_render_gen_name(autogen_context, op.constraint_name))]
+ if not autogen_context._has_batch:
+ args.append(repr(_ident(op.source_table)))
+
+ args.extend(
+ [
+ repr(_ident(op.referent_table)),
+ repr([_ident(col) for col in op.local_cols]),
+ repr([_ident(col) for col in op.remote_cols]),
+ ]
+ )
+ kwargs = [
+ "referent_schema",
+ "onupdate",
+ "ondelete",
+ "initially",
+ "deferrable",
+ "use_alter",
+ "match",
+ ]
+ if not autogen_context._has_batch:
+ kwargs.insert(0, "source_schema")
+
+ for k in kwargs:
+ if k in op.kw:
+ value = op.kw[k]
+ if value is not None:
+ args.append("%s=%r" % (k, value))
+
+ return "%(prefix)screate_foreign_key(%(args)s)" % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "args": ", ".join(args),
+ }
+
+
+@renderers.dispatch_for(ops.CreatePrimaryKeyOp)
+def _add_pk_constraint(constraint, autogen_context):
+ raise NotImplementedError()
+
+
+@renderers.dispatch_for(ops.CreateCheckConstraintOp)
+def _add_check_constraint(constraint, autogen_context):
+ raise NotImplementedError()
+
+
+@renderers.dispatch_for(ops.DropConstraintOp)
+def _drop_constraint(
+ autogen_context: AutogenContext, op: ops.DropConstraintOp
+) -> str:
+ prefix = _alembic_autogenerate_prefix(autogen_context)
+ name = _render_gen_name(autogen_context, op.constraint_name)
+ schema = _ident(op.schema) if op.schema else None
+ type_ = _ident(op.constraint_type) if op.constraint_type else None
+
+ params_strs = []
+ params_strs.append(repr(name))
+ if not autogen_context._has_batch:
+ params_strs.append(repr(_ident(op.table_name)))
+ if schema is not None:
+ params_strs.append(f"schema={schema!r}")
+ if type_ is not None:
+ params_strs.append(f"type_={type_!r}")
+
+ return f"{prefix}drop_constraint({', '.join(params_strs)})"
+
+
+@renderers.dispatch_for(ops.AddColumnOp)
+def _add_column(autogen_context: AutogenContext, op: ops.AddColumnOp) -> str:
+ schema, tname, column = op.schema, op.table_name, op.column
+ if autogen_context._has_batch:
+ template = "%(prefix)sadd_column(%(column)s)"
+ else:
+ template = "%(prefix)sadd_column(%(tname)r, %(column)s"
+ if schema:
+ template += ", schema=%(schema)r"
+ template += ")"
+ text = template % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "tname": tname,
+ "column": _render_column(column, autogen_context),
+ "schema": schema,
+ }
+ return text
+
+
+@renderers.dispatch_for(ops.DropColumnOp)
+def _drop_column(autogen_context: AutogenContext, op: ops.DropColumnOp) -> str:
+ schema, tname, column_name = op.schema, op.table_name, op.column_name
+
+ if autogen_context._has_batch:
+ template = "%(prefix)sdrop_column(%(cname)r)"
+ else:
+ template = "%(prefix)sdrop_column(%(tname)r, %(cname)r"
+ if schema:
+ template += ", schema=%(schema)r"
+ template += ")"
+
+ text = template % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "tname": _ident(tname),
+ "cname": _ident(column_name),
+ "schema": _ident(schema),
+ }
+ return text
+
+
+@renderers.dispatch_for(ops.AlterColumnOp)
+def _alter_column(
+ autogen_context: AutogenContext, op: ops.AlterColumnOp
+) -> str:
+ tname = op.table_name
+ cname = op.column_name
+ server_default = op.modify_server_default
+ type_ = op.modify_type
+ nullable = op.modify_nullable
+ comment = op.modify_comment
+ autoincrement = op.kw.get("autoincrement", None)
+ existing_type = op.existing_type
+ existing_nullable = op.existing_nullable
+ existing_comment = op.existing_comment
+ existing_server_default = op.existing_server_default
+ schema = op.schema
+
+ indent = " " * 11
+
+ if autogen_context._has_batch:
+ template = "%(prefix)salter_column(%(cname)r"
+ else:
+ template = "%(prefix)salter_column(%(tname)r, %(cname)r"
+
+ text = template % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "tname": tname,
+ "cname": cname,
+ }
+ if existing_type is not None:
+ text += ",\n%sexisting_type=%s" % (
+ indent,
+ _repr_type(existing_type, autogen_context),
+ )
+ if server_default is not False:
+ rendered = _render_server_default(server_default, autogen_context)
+ text += ",\n%sserver_default=%s" % (indent, rendered)
+
+ if type_ is not None:
+ text += ",\n%stype_=%s" % (indent, _repr_type(type_, autogen_context))
+ if nullable is not None:
+ text += ",\n%snullable=%r" % (indent, nullable)
+ if comment is not False:
+ text += ",\n%scomment=%r" % (indent, comment)
+ if existing_comment is not None:
+ text += ",\n%sexisting_comment=%r" % (indent, existing_comment)
+ if nullable is None and existing_nullable is not None:
+ text += ",\n%sexisting_nullable=%r" % (indent, existing_nullable)
+ if autoincrement is not None:
+ text += ",\n%sautoincrement=%r" % (indent, autoincrement)
+ if server_default is False and existing_server_default:
+ rendered = _render_server_default(
+ existing_server_default, autogen_context
+ )
+ text += ",\n%sexisting_server_default=%s" % (indent, rendered)
+ if schema and not autogen_context._has_batch:
+ text += ",\n%sschema=%r" % (indent, schema)
+ text += ")"
+ return text
+
+
+class _f_name:
+ def __init__(self, prefix: str, name: conv) -> None:
+ self.prefix = prefix
+ self.name = name
+
+ def __repr__(self) -> str:
+ return "%sf(%r)" % (self.prefix, _ident(self.name))
+
+
+def _ident(name: Optional[Union[quoted_name, str]]) -> Optional[str]:
+ """produce a __repr__() object for a string identifier that may
+ use quoted_name() in SQLAlchemy 0.9 and greater.
+
+ The issue worked around here is that quoted_name() doesn't have
+ very good repr() behavior by itself when unicode is involved.
+
+ """
+ if name is None:
+ return name
+ elif isinstance(name, quoted_name):
+ return str(name)
+ elif isinstance(name, str):
+ return name
+
+
+def _render_potential_expr(
+ value: Any,
+ autogen_context: AutogenContext,
+ *,
+ wrap_in_text: bool = True,
+ is_server_default: bool = False,
+ is_index: bool = False,
+) -> str:
+ if isinstance(value, sql.ClauseElement):
+ if wrap_in_text:
+ template = "%(prefix)stext(%(sql)r)"
+ else:
+ template = "%(sql)r"
+
+ return template % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "sql": autogen_context.migration_context.impl.render_ddl_sql_expr(
+ value, is_server_default=is_server_default, is_index=is_index
+ ),
+ }
+
+ else:
+ return repr(value)
+
+
+def _get_index_rendered_expressions(
+ idx: Index, autogen_context: AutogenContext
+) -> List[str]:
+ return [
+ repr(_ident(getattr(exp, "name", None)))
+ if isinstance(exp, sa_schema.Column)
+ else _render_potential_expr(exp, autogen_context, is_index=True)
+ for exp in idx.expressions
+ ]
+
+
+def _uq_constraint(
+ constraint: UniqueConstraint,
+ autogen_context: AutogenContext,
+ alter: bool,
+) -> str:
+ opts: List[Tuple[str, Any]] = []
+
+ has_batch = autogen_context._has_batch
+
+ if constraint.deferrable:
+ opts.append(("deferrable", str(constraint.deferrable)))
+ if constraint.initially:
+ opts.append(("initially", str(constraint.initially)))
+ if not has_batch and alter and constraint.table.schema:
+ opts.append(("schema", _ident(constraint.table.schema)))
+ if not alter and constraint.name:
+ opts.append(
+ ("name", _render_gen_name(autogen_context, constraint.name))
+ )
+ dialect_options = _render_dialect_kwargs_items(autogen_context, constraint)
+
+ if alter:
+ args = [repr(_render_gen_name(autogen_context, constraint.name))]
+ if not has_batch:
+ args += [repr(_ident(constraint.table.name))]
+ args.append(repr([_ident(col.name) for col in constraint.columns]))
+ args.extend(["%s=%r" % (k, v) for k, v in opts])
+ args.extend(dialect_options)
+ return "%(prefix)screate_unique_constraint(%(args)s)" % {
+ "prefix": _alembic_autogenerate_prefix(autogen_context),
+ "args": ", ".join(args),
+ }
+ else:
+ args = [repr(_ident(col.name)) for col in constraint.columns]
+ args.extend(["%s=%r" % (k, v) for k, v in opts])
+ args.extend(dialect_options)
+ return "%(prefix)sUniqueConstraint(%(args)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "args": ", ".join(args),
+ }
+
+
+def _user_autogenerate_prefix(autogen_context, target):
+ prefix = autogen_context.opts["user_module_prefix"]
+ if prefix is None:
+ return "%s." % target.__module__
+ else:
+ return prefix
+
+
+def _sqlalchemy_autogenerate_prefix(autogen_context: AutogenContext) -> str:
+ return autogen_context.opts["sqlalchemy_module_prefix"] or ""
+
+
+def _alembic_autogenerate_prefix(autogen_context: AutogenContext) -> str:
+ if autogen_context._has_batch:
+ return "batch_op."
+ else:
+ return autogen_context.opts["alembic_module_prefix"] or ""
+
+
+def _user_defined_render(
+ type_: str, object_: Any, autogen_context: AutogenContext
+) -> Union[str, Literal[False]]:
+ if "render_item" in autogen_context.opts:
+ render = autogen_context.opts["render_item"]
+ if render:
+ rendered = render(type_, object_, autogen_context)
+ if rendered is not False:
+ return rendered
+ return False
+
+
+def _render_column(
+ column: Column[Any], autogen_context: AutogenContext
+) -> str:
+ rendered = _user_defined_render("column", column, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ args: List[str] = []
+ opts: List[Tuple[str, Any]] = []
+
+ if column.server_default:
+ rendered = _render_server_default( # type:ignore[assignment]
+ column.server_default, autogen_context
+ )
+ if rendered:
+ if _should_render_server_default_positionally(
+ column.server_default
+ ):
+ args.append(rendered)
+ else:
+ opts.append(("server_default", rendered))
+
+ if (
+ column.autoincrement is not None
+ and column.autoincrement != sqla_compat.AUTOINCREMENT_DEFAULT
+ ):
+ opts.append(("autoincrement", column.autoincrement))
+
+ if column.nullable is not None:
+ opts.append(("nullable", column.nullable))
+
+ if column.system:
+ opts.append(("system", column.system))
+
+ comment = column.comment
+ if comment:
+ opts.append(("comment", "%r" % comment))
+
+ # TODO: for non-ascii colname, assign a "key"
+ return "%(prefix)sColumn(%(name)r, %(type)s, %(args)s%(kwargs)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "name": _ident(column.name),
+ "type": _repr_type(column.type, autogen_context),
+ "args": ", ".join([str(arg) for arg in args]) + ", " if args else "",
+ "kwargs": (
+ ", ".join(
+ ["%s=%s" % (kwname, val) for kwname, val in opts]
+ + [
+ "%s=%s"
+ % (key, _render_potential_expr(val, autogen_context))
+ for key, val in sqla_compat._column_kwargs(column).items()
+ ]
+ )
+ ),
+ }
+
+
+def _should_render_server_default_positionally(server_default: Any) -> bool:
+ return sqla_compat._server_default_is_computed(
+ server_default
+ ) or sqla_compat._server_default_is_identity(server_default)
+
+
+def _render_server_default(
+ default: Optional[
+ Union[FetchedValue, str, TextClause, ColumnElement[Any]]
+ ],
+ autogen_context: AutogenContext,
+ repr_: bool = True,
+) -> Optional[str]:
+ rendered = _user_defined_render("server_default", default, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ if sqla_compat._server_default_is_computed(default):
+ return _render_computed(cast("Computed", default), autogen_context)
+ elif sqla_compat._server_default_is_identity(default):
+ return _render_identity(cast("Identity", default), autogen_context)
+ elif isinstance(default, sa_schema.DefaultClause):
+ if isinstance(default.arg, str):
+ default = default.arg
+ else:
+ return _render_potential_expr(
+ default.arg, autogen_context, is_server_default=True
+ )
+
+ if isinstance(default, str) and repr_:
+ default = repr(re.sub(r"^'|'$", "", default))
+
+ return cast(str, default)
+
+
+def _render_computed(
+ computed: Computed, autogen_context: AutogenContext
+) -> str:
+ text = _render_potential_expr(
+ computed.sqltext, autogen_context, wrap_in_text=False
+ )
+
+ kwargs = {}
+ if computed.persisted is not None:
+ kwargs["persisted"] = computed.persisted
+ return "%(prefix)sComputed(%(text)s, %(kwargs)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "text": text,
+ "kwargs": (", ".join("%s=%s" % pair for pair in kwargs.items())),
+ }
+
+
+def _render_identity(
+ identity: Identity, autogen_context: AutogenContext
+) -> str:
+ kwargs = sqla_compat._get_identity_options_dict(
+ identity, dialect_kwargs=True
+ )
+
+ return "%(prefix)sIdentity(%(kwargs)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "kwargs": (", ".join("%s=%s" % pair for pair in kwargs.items())),
+ }
+
+
+def _repr_type(
+ type_: TypeEngine,
+ autogen_context: AutogenContext,
+ _skip_variants: bool = False,
+) -> str:
+ rendered = _user_defined_render("type", type_, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ if hasattr(autogen_context.migration_context, "impl"):
+ impl_rt = autogen_context.migration_context.impl.render_type(
+ type_, autogen_context
+ )
+ else:
+ impl_rt = None
+
+ mod = type(type_).__module__
+ imports = autogen_context.imports
+ if mod.startswith("sqlalchemy.dialects"):
+ match = re.match(r"sqlalchemy\.dialects\.(\w+)", mod)
+ assert match is not None
+ dname = match.group(1)
+ if imports is not None:
+ imports.add("from sqlalchemy.dialects import %s" % dname)
+ if impl_rt:
+ return impl_rt
+ else:
+ return "%s.%r" % (dname, type_)
+ elif impl_rt:
+ return impl_rt
+ elif not _skip_variants and sqla_compat._type_has_variants(type_):
+ return _render_Variant_type(type_, autogen_context)
+ elif mod.startswith("sqlalchemy."):
+ if "_render_%s_type" % type_.__visit_name__ in globals():
+ fn = globals()["_render_%s_type" % type_.__visit_name__]
+ return fn(type_, autogen_context)
+ else:
+ prefix = _sqlalchemy_autogenerate_prefix(autogen_context)
+ return "%s%r" % (prefix, type_)
+ else:
+ prefix = _user_autogenerate_prefix(autogen_context, type_)
+ return "%s%r" % (prefix, type_)
+
+
+def _render_ARRAY_type(type_: ARRAY, autogen_context: AutogenContext) -> str:
+ return cast(
+ str,
+ _render_type_w_subtype(
+ type_, autogen_context, "item_type", r"(.+?\()"
+ ),
+ )
+
+
+def _render_Variant_type(
+ type_: TypeEngine, autogen_context: AutogenContext
+) -> str:
+ base_type, variant_mapping = sqla_compat._get_variant_mapping(type_)
+ base = _repr_type(base_type, autogen_context, _skip_variants=True)
+ assert base is not None and base is not False # type: ignore[comparison-overlap] # noqa:E501
+ for dialect in sorted(variant_mapping):
+ typ = variant_mapping[dialect]
+ base += ".with_variant(%s, %r)" % (
+ _repr_type(typ, autogen_context, _skip_variants=True),
+ dialect,
+ )
+ return base
+
+
+def _render_type_w_subtype(
+ type_: TypeEngine,
+ autogen_context: AutogenContext,
+ attrname: str,
+ regexp: str,
+ prefix: Optional[str] = None,
+) -> Union[Optional[str], Literal[False]]:
+ outer_repr = repr(type_)
+ inner_type = getattr(type_, attrname, None)
+ if inner_type is None:
+ return False
+
+ inner_repr = repr(inner_type)
+
+ inner_repr = re.sub(r"([\(\)])", r"\\\1", inner_repr)
+ sub_type = _repr_type(getattr(type_, attrname), autogen_context)
+ outer_type = re.sub(regexp + inner_repr, r"\1%s" % sub_type, outer_repr)
+
+ if prefix:
+ return "%s%s" % (prefix, outer_type)
+
+ mod = type(type_).__module__
+ if mod.startswith("sqlalchemy.dialects"):
+ match = re.match(r"sqlalchemy\.dialects\.(\w+)", mod)
+ assert match is not None
+ dname = match.group(1)
+ return "%s.%s" % (dname, outer_type)
+ elif mod.startswith("sqlalchemy"):
+ prefix = _sqlalchemy_autogenerate_prefix(autogen_context)
+ return "%s%s" % (prefix, outer_type)
+ else:
+ return None
+
+
+_constraint_renderers = util.Dispatcher()
+
+
+def _render_constraint(
+ constraint: Constraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: Optional[MetaData],
+) -> Optional[str]:
+ try:
+ renderer = _constraint_renderers.dispatch(constraint)
+ except ValueError:
+ util.warn("No renderer is established for object %r" % constraint)
+ return "[Unknown Python object %r]" % constraint
+ else:
+ return renderer(constraint, autogen_context, namespace_metadata)
+
+
+@_constraint_renderers.dispatch_for(sa_schema.PrimaryKeyConstraint)
+def _render_primary_key(
+ constraint: PrimaryKeyConstraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: Optional[MetaData],
+) -> Optional[str]:
+ rendered = _user_defined_render("primary_key", constraint, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ if not constraint.columns:
+ return None
+
+ opts = []
+ if constraint.name:
+ opts.append(
+ ("name", repr(_render_gen_name(autogen_context, constraint.name)))
+ )
+ return "%(prefix)sPrimaryKeyConstraint(%(args)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "args": ", ".join(
+ [repr(c.name) for c in constraint.columns]
+ + ["%s=%s" % (kwname, val) for kwname, val in opts]
+ ),
+ }
+
+
+def _fk_colspec(
+ fk: ForeignKey,
+ metadata_schema: Optional[str],
+ namespace_metadata: MetaData,
+) -> str:
+ """Implement a 'safe' version of ForeignKey._get_colspec() that
+ won't fail if the remote table can't be resolved.
+
+ """
+ colspec = fk._get_colspec()
+ tokens = colspec.split(".")
+ tname, colname = tokens[-2:]
+
+ if metadata_schema is not None and len(tokens) == 2:
+ table_fullname = "%s.%s" % (metadata_schema, tname)
+ else:
+ table_fullname = ".".join(tokens[0:-1])
+
+ if (
+ not fk.link_to_name
+ and fk.parent is not None
+ and fk.parent.table is not None
+ ):
+ # try to resolve the remote table in order to adjust for column.key.
+ # the FK constraint needs to be rendered in terms of the column
+ # name.
+
+ if table_fullname in namespace_metadata.tables:
+ col = namespace_metadata.tables[table_fullname].c.get(colname)
+ if col is not None:
+ colname = _ident(col.name) # type: ignore[assignment]
+
+ colspec = "%s.%s" % (table_fullname, colname)
+
+ return colspec
+
+
+def _populate_render_fk_opts(
+ constraint: ForeignKeyConstraint, opts: List[Tuple[str, str]]
+) -> None:
+ if constraint.onupdate:
+ opts.append(("onupdate", repr(constraint.onupdate)))
+ if constraint.ondelete:
+ opts.append(("ondelete", repr(constraint.ondelete)))
+ if constraint.initially:
+ opts.append(("initially", repr(constraint.initially)))
+ if constraint.deferrable:
+ opts.append(("deferrable", repr(constraint.deferrable)))
+ if constraint.use_alter:
+ opts.append(("use_alter", repr(constraint.use_alter)))
+ if constraint.match:
+ opts.append(("match", repr(constraint.match)))
+
+
+@_constraint_renderers.dispatch_for(sa_schema.ForeignKeyConstraint)
+def _render_foreign_key(
+ constraint: ForeignKeyConstraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: MetaData,
+) -> Optional[str]:
+ rendered = _user_defined_render("foreign_key", constraint, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ opts = []
+ if constraint.name:
+ opts.append(
+ ("name", repr(_render_gen_name(autogen_context, constraint.name)))
+ )
+
+ _populate_render_fk_opts(constraint, opts)
+
+ apply_metadata_schema = namespace_metadata.schema
+ return (
+ "%(prefix)sForeignKeyConstraint([%(cols)s], "
+ "[%(refcols)s], %(args)s)"
+ % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "cols": ", ".join(
+ repr(_ident(f.parent.name)) for f in constraint.elements
+ ),
+ "refcols": ", ".join(
+ repr(_fk_colspec(f, apply_metadata_schema, namespace_metadata))
+ for f in constraint.elements
+ ),
+ "args": ", ".join(
+ ["%s=%s" % (kwname, val) for kwname, val in opts]
+ ),
+ }
+ )
+
+
+@_constraint_renderers.dispatch_for(sa_schema.UniqueConstraint)
+def _render_unique_constraint(
+ constraint: UniqueConstraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: Optional[MetaData],
+) -> str:
+ rendered = _user_defined_render("unique", constraint, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ return _uq_constraint(constraint, autogen_context, False)
+
+
+@_constraint_renderers.dispatch_for(sa_schema.CheckConstraint)
+def _render_check_constraint(
+ constraint: CheckConstraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: Optional[MetaData],
+) -> Optional[str]:
+ rendered = _user_defined_render("check", constraint, autogen_context)
+ if rendered is not False:
+ return rendered
+
+ # detect the constraint being part of
+ # a parent type which is probably in the Table already.
+ # ideally SQLAlchemy would give us more of a first class
+ # way to detect this.
+ if (
+ constraint._create_rule
+ and hasattr(constraint._create_rule, "target")
+ and isinstance(
+ constraint._create_rule.target,
+ sqltypes.TypeEngine,
+ )
+ ):
+ return None
+ opts = []
+ if constraint.name:
+ opts.append(
+ ("name", repr(_render_gen_name(autogen_context, constraint.name)))
+ )
+ return "%(prefix)sCheckConstraint(%(sqltext)s%(opts)s)" % {
+ "prefix": _sqlalchemy_autogenerate_prefix(autogen_context),
+ "opts": ", " + (", ".join("%s=%s" % (k, v) for k, v in opts))
+ if opts
+ else "",
+ "sqltext": _render_potential_expr(
+ constraint.sqltext, autogen_context, wrap_in_text=False
+ ),
+ }
+
+
+@renderers.dispatch_for(ops.ExecuteSQLOp)
+def _execute_sql(autogen_context: AutogenContext, op: ops.ExecuteSQLOp) -> str:
+ if not isinstance(op.sqltext, str):
+ raise NotImplementedError(
+ "Autogenerate rendering of SQL Expression language constructs "
+ "not supported here; please use a plain SQL string"
+ )
+ return "op.execute(%r)" % op.sqltext
+
+
+renderers = default_renderers.branch()
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/rewriter.py b/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/rewriter.py
new file mode 100644
index 00000000..8994dcf8
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/autogenerate/rewriter.py
@@ -0,0 +1,240 @@
+from __future__ import annotations
+
+from typing import Any
+from typing import Callable
+from typing import Iterator
+from typing import List
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+from .. import util
+from ..operations import ops
+
+if TYPE_CHECKING:
+ from ..operations.ops import AddColumnOp
+ from ..operations.ops import AlterColumnOp
+ from ..operations.ops import CreateTableOp
+ from ..operations.ops import DowngradeOps
+ from ..operations.ops import MigrateOperation
+ from ..operations.ops import MigrationScript
+ from ..operations.ops import ModifyTableOps
+ from ..operations.ops import OpContainer
+ from ..operations.ops import UpgradeOps
+ from ..runtime.migration import MigrationContext
+ from ..script.revision import _GetRevArg
+
+ProcessRevisionDirectiveFn = Callable[
+ ["MigrationContext", "_GetRevArg", List["MigrationScript"]], None
+]
+
+
+class Rewriter:
+ """A helper object that allows easy 'rewriting' of ops streams.
+
+ The :class:`.Rewriter` object is intended to be passed along
+ to the
+ :paramref:`.EnvironmentContext.configure.process_revision_directives`
+ parameter in an ``env.py`` script. Once constructed, any number
+ of "rewrites" functions can be associated with it, which will be given
+ the opportunity to modify the structure without having to have explicit
+ knowledge of the overall structure.
+
+ The function is passed the :class:`.MigrationContext` object and
+ ``revision`` tuple that are passed to the :paramref:`.Environment
+ Context.configure.process_revision_directives` function normally,
+ and the third argument is an individual directive of the type
+ noted in the decorator. The function has the choice of returning
+ a single op directive, which normally can be the directive that
+ was actually passed, or a new directive to replace it, or a list
+ of zero or more directives to replace it.
+
+ .. seealso::
+
+ :ref:`autogen_rewriter` - usage example
+
+ """
+
+ _traverse = util.Dispatcher()
+
+ _chained: Tuple[Union[ProcessRevisionDirectiveFn, Rewriter], ...] = ()
+
+ def __init__(self) -> None:
+ self.dispatch = util.Dispatcher()
+
+ def chain(
+ self,
+ other: Union[
+ ProcessRevisionDirectiveFn,
+ Rewriter,
+ ],
+ ) -> Rewriter:
+ """Produce a "chain" of this :class:`.Rewriter` to another.
+
+ This allows two or more rewriters to operate serially on a stream,
+ e.g.::
+
+ writer1 = autogenerate.Rewriter()
+ writer2 = autogenerate.Rewriter()
+
+
+ @writer1.rewrites(ops.AddColumnOp)
+ def add_column_nullable(context, revision, op):
+ op.column.nullable = True
+ return op
+
+
+ @writer2.rewrites(ops.AddColumnOp)
+ def add_column_idx(context, revision, op):
+ idx_op = ops.CreateIndexOp(
+ "ixc", op.table_name, [op.column.name]
+ )
+ return [op, idx_op]
+
+ writer = writer1.chain(writer2)
+
+ :param other: a :class:`.Rewriter` instance
+ :return: a new :class:`.Rewriter` that will run the operations
+ of this writer, then the "other" writer, in succession.
+
+ """
+ wr = self.__class__.__new__(self.__class__)
+ wr.__dict__.update(self.__dict__)
+ wr._chained += (other,)
+ return wr
+
+ def rewrites(
+ self,
+ operator: Union[
+ Type[AddColumnOp],
+ Type[MigrateOperation],
+ Type[AlterColumnOp],
+ Type[CreateTableOp],
+ Type[ModifyTableOps],
+ ],
+ ) -> Callable[..., Any]:
+ """Register a function as rewriter for a given type.
+
+ The function should receive three arguments, which are
+ the :class:`.MigrationContext`, a ``revision`` tuple, and
+ an op directive of the type indicated. E.g.::
+
+ @writer1.rewrites(ops.AddColumnOp)
+ def add_column_nullable(context, revision, op):
+ op.column.nullable = True
+ return op
+
+ """
+ return self.dispatch.dispatch_for(operator)
+
+ def _rewrite(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directive: MigrateOperation,
+ ) -> Iterator[MigrateOperation]:
+ try:
+ _rewriter = self.dispatch.dispatch(directive)
+ except ValueError:
+ _rewriter = None
+ yield directive
+ else:
+ if self in directive._mutations:
+ yield directive
+ else:
+ for r_directive in util.to_list(
+ _rewriter(context, revision, directive), []
+ ):
+ r_directive._mutations = r_directive._mutations.union(
+ [self]
+ )
+ yield r_directive
+
+ def __call__(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directives: List[MigrationScript],
+ ) -> None:
+ self.process_revision_directives(context, revision, directives)
+ for process_revision_directives in self._chained:
+ process_revision_directives(context, revision, directives)
+
+ @_traverse.dispatch_for(ops.MigrationScript)
+ def _traverse_script(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directive: MigrationScript,
+ ) -> None:
+ upgrade_ops_list: List[UpgradeOps] = []
+ for upgrade_ops in directive.upgrade_ops_list:
+ ret = self._traverse_for(context, revision, upgrade_ops)
+ if len(ret) != 1:
+ raise ValueError(
+ "Can only return single object for UpgradeOps traverse"
+ )
+ upgrade_ops_list.append(ret[0])
+
+ directive.upgrade_ops = upgrade_ops_list # type: ignore
+
+ downgrade_ops_list: List[DowngradeOps] = []
+ for downgrade_ops in directive.downgrade_ops_list:
+ ret = self._traverse_for(context, revision, downgrade_ops)
+ if len(ret) != 1:
+ raise ValueError(
+ "Can only return single object for DowngradeOps traverse"
+ )
+ downgrade_ops_list.append(ret[0])
+ directive.downgrade_ops = downgrade_ops_list # type: ignore
+
+ @_traverse.dispatch_for(ops.OpContainer)
+ def _traverse_op_container(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directive: OpContainer,
+ ) -> None:
+ self._traverse_list(context, revision, directive.ops)
+
+ @_traverse.dispatch_for(ops.MigrateOperation)
+ def _traverse_any_directive(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directive: MigrateOperation,
+ ) -> None:
+ pass
+
+ def _traverse_for(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directive: MigrateOperation,
+ ) -> Any:
+ directives = list(self._rewrite(context, revision, directive))
+ for directive in directives:
+ traverser = self._traverse.dispatch(directive)
+ traverser(self, context, revision, directive)
+ return directives
+
+ def _traverse_list(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directives: Any,
+ ) -> None:
+ dest = []
+ for directive in directives:
+ dest.extend(self._traverse_for(context, revision, directive))
+
+ directives[:] = dest
+
+ def process_revision_directives(
+ self,
+ context: MigrationContext,
+ revision: _GetRevArg,
+ directives: List[MigrationScript],
+ ) -> None:
+ self._traverse_list(context, revision, directives)
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/command.py b/backend/venv/lib/python3.9/site-packages/alembic/command.py
new file mode 100644
index 00000000..37aa6e67
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/command.py
@@ -0,0 +1,749 @@
+# mypy: allow-untyped-defs, allow-untyped-calls
+
+from __future__ import annotations
+
+import os
+from typing import List
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from . import autogenerate as autogen
+from . import util
+from .runtime.environment import EnvironmentContext
+from .script import ScriptDirectory
+
+if TYPE_CHECKING:
+ from alembic.config import Config
+ from alembic.script.base import Script
+ from alembic.script.revision import _RevIdType
+ from .runtime.environment import ProcessRevisionDirectiveFn
+
+
+def list_templates(config: Config) -> None:
+ """List available templates.
+
+ :param config: a :class:`.Config` object.
+
+ """
+
+ config.print_stdout("Available templates:\n")
+ for tempname in os.listdir(config.get_template_directory()):
+ with open(
+ os.path.join(config.get_template_directory(), tempname, "README")
+ ) as readme:
+ synopsis = next(readme).rstrip()
+ config.print_stdout("%s - %s", tempname, synopsis)
+
+ config.print_stdout("\nTemplates are used via the 'init' command, e.g.:")
+ config.print_stdout("\n alembic init --template generic ./scripts")
+
+
+def init(
+ config: Config,
+ directory: str,
+ template: str = "generic",
+ package: bool = False,
+) -> None:
+ """Initialize a new scripts directory.
+
+ :param config: a :class:`.Config` object.
+
+ :param directory: string path of the target directory
+
+ :param template: string name of the migration environment template to
+ use.
+
+ :param package: when True, write ``__init__.py`` files into the
+ environment location as well as the versions/ location.
+
+ """
+
+ if os.access(directory, os.F_OK) and os.listdir(directory):
+ raise util.CommandError(
+ "Directory %s already exists and is not empty" % directory
+ )
+
+ template_dir = os.path.join(config.get_template_directory(), template)
+ if not os.access(template_dir, os.F_OK):
+ raise util.CommandError("No such template %r" % template)
+
+ if not os.access(directory, os.F_OK):
+ with util.status(
+ f"Creating directory {os.path.abspath(directory)!r}",
+ **config.messaging_opts,
+ ):
+ os.makedirs(directory)
+
+ versions = os.path.join(directory, "versions")
+ with util.status(
+ f"Creating directory {os.path.abspath(versions)!r}",
+ **config.messaging_opts,
+ ):
+ os.makedirs(versions)
+
+ script = ScriptDirectory(directory)
+
+ config_file: str | None = None
+ for file_ in os.listdir(template_dir):
+ file_path = os.path.join(template_dir, file_)
+ if file_ == "alembic.ini.mako":
+ assert config.config_file_name is not None
+ config_file = os.path.abspath(config.config_file_name)
+ if os.access(config_file, os.F_OK):
+ util.msg(
+ f"File {config_file!r} already exists, skipping",
+ **config.messaging_opts,
+ )
+ else:
+ script._generate_template(
+ file_path, config_file, script_location=directory
+ )
+ elif os.path.isfile(file_path):
+ output_file = os.path.join(directory, file_)
+ script._copy_file(file_path, output_file)
+
+ if package:
+ for path in [
+ os.path.join(os.path.abspath(directory), "__init__.py"),
+ os.path.join(os.path.abspath(versions), "__init__.py"),
+ ]:
+ with util.status(f"Adding {path!r}", **config.messaging_opts):
+ with open(path, "w"):
+ pass
+
+ assert config_file is not None
+ util.msg(
+ "Please edit configuration/connection/logging "
+ f"settings in {config_file!r} before proceeding.",
+ **config.messaging_opts,
+ )
+
+
+def revision(
+ config: Config,
+ message: Optional[str] = None,
+ autogenerate: bool = False,
+ sql: bool = False,
+ head: str = "head",
+ splice: bool = False,
+ branch_label: Optional[_RevIdType] = None,
+ version_path: Optional[str] = None,
+ rev_id: Optional[str] = None,
+ depends_on: Optional[str] = None,
+ process_revision_directives: Optional[ProcessRevisionDirectiveFn] = None,
+) -> Union[Optional[Script], List[Optional[Script]]]:
+ """Create a new revision file.
+
+ :param config: a :class:`.Config` object.
+
+ :param message: string message to apply to the revision; this is the
+ ``-m`` option to ``alembic revision``.
+
+ :param autogenerate: whether or not to autogenerate the script from
+ the database; this is the ``--autogenerate`` option to
+ ``alembic revision``.
+
+ :param sql: whether to dump the script out as a SQL string; when specified,
+ the script is dumped to stdout. This is the ``--sql`` option to
+ ``alembic revision``.
+
+ :param head: head revision to build the new revision upon as a parent;
+ this is the ``--head`` option to ``alembic revision``.
+
+ :param splice: whether or not the new revision should be made into a
+ new head of its own; is required when the given ``head`` is not itself
+ a head. This is the ``--splice`` option to ``alembic revision``.
+
+ :param branch_label: string label to apply to the branch; this is the
+ ``--branch-label`` option to ``alembic revision``.
+
+ :param version_path: string symbol identifying a specific version path
+ from the configuration; this is the ``--version-path`` option to
+ ``alembic revision``.
+
+ :param rev_id: optional revision identifier to use instead of having
+ one generated; this is the ``--rev-id`` option to ``alembic revision``.
+
+ :param depends_on: optional list of "depends on" identifiers; this is the
+ ``--depends-on`` option to ``alembic revision``.
+
+ :param process_revision_directives: this is a callable that takes the
+ same form as the callable described at
+ :paramref:`.EnvironmentContext.configure.process_revision_directives`;
+ will be applied to the structure generated by the revision process
+ where it can be altered programmatically. Note that unlike all
+ the other parameters, this option is only available via programmatic
+ use of :func:`.command.revision`
+
+ """
+
+ script_directory = ScriptDirectory.from_config(config)
+
+ command_args = dict(
+ message=message,
+ autogenerate=autogenerate,
+ sql=sql,
+ head=head,
+ splice=splice,
+ branch_label=branch_label,
+ version_path=version_path,
+ rev_id=rev_id,
+ depends_on=depends_on,
+ )
+ revision_context = autogen.RevisionContext(
+ config,
+ script_directory,
+ command_args,
+ process_revision_directives=process_revision_directives,
+ )
+
+ environment = util.asbool(config.get_main_option("revision_environment"))
+
+ if autogenerate:
+ environment = True
+
+ if sql:
+ raise util.CommandError(
+ "Using --sql with --autogenerate does not make any sense"
+ )
+
+ def retrieve_migrations(rev, context):
+ revision_context.run_autogenerate(rev, context)
+ return []
+
+ elif environment:
+
+ def retrieve_migrations(rev, context):
+ revision_context.run_no_autogenerate(rev, context)
+ return []
+
+ elif sql:
+ raise util.CommandError(
+ "Using --sql with the revision command when "
+ "revision_environment is not configured does not make any sense"
+ )
+
+ if environment:
+ with EnvironmentContext(
+ config,
+ script_directory,
+ fn=retrieve_migrations,
+ as_sql=sql,
+ template_args=revision_context.template_args,
+ revision_context=revision_context,
+ ):
+ script_directory.run_env()
+
+ # the revision_context now has MigrationScript structure(s) present.
+ # these could theoretically be further processed / rewritten *here*,
+ # in addition to the hooks present within each run_migrations() call,
+ # or at the end of env.py run_migrations_online().
+
+ scripts = [script for script in revision_context.generate_scripts()]
+ if len(scripts) == 1:
+ return scripts[0]
+ else:
+ return scripts
+
+
+def check(config: "Config") -> None:
+ """Check if revision command with autogenerate has pending upgrade ops.
+
+ :param config: a :class:`.Config` object.
+
+ .. versionadded:: 1.9.0
+
+ """
+
+ script_directory = ScriptDirectory.from_config(config)
+
+ command_args = dict(
+ message=None,
+ autogenerate=True,
+ sql=False,
+ head="head",
+ splice=False,
+ branch_label=None,
+ version_path=None,
+ rev_id=None,
+ depends_on=None,
+ )
+ revision_context = autogen.RevisionContext(
+ config,
+ script_directory,
+ command_args,
+ )
+
+ def retrieve_migrations(rev, context):
+ revision_context.run_autogenerate(rev, context)
+ return []
+
+ with EnvironmentContext(
+ config,
+ script_directory,
+ fn=retrieve_migrations,
+ as_sql=False,
+ template_args=revision_context.template_args,
+ revision_context=revision_context,
+ ):
+ script_directory.run_env()
+
+ # the revision_context now has MigrationScript structure(s) present.
+
+ migration_script = revision_context.generated_revisions[-1]
+ diffs = []
+ for upgrade_ops in migration_script.upgrade_ops_list:
+ diffs.extend(upgrade_ops.as_diffs())
+
+ if diffs:
+ raise util.AutogenerateDiffsDetected(
+ f"New upgrade operations detected: {diffs}"
+ )
+ else:
+ config.print_stdout("No new upgrade operations detected.")
+
+
+def merge(
+ config: Config,
+ revisions: _RevIdType,
+ message: Optional[str] = None,
+ branch_label: Optional[_RevIdType] = None,
+ rev_id: Optional[str] = None,
+) -> Optional[Script]:
+ """Merge two revisions together. Creates a new migration file.
+
+ :param config: a :class:`.Config` instance
+
+ :param message: string message to apply to the revision
+
+ :param branch_label: string label name to apply to the new revision
+
+ :param rev_id: hardcoded revision identifier instead of generating a new
+ one.
+
+ .. seealso::
+
+ :ref:`branches`
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+ template_args = {
+ "config": config # Let templates use config for
+ # e.g. multiple databases
+ }
+
+ environment = util.asbool(config.get_main_option("revision_environment"))
+
+ if environment:
+
+ def nothing(rev, context):
+ return []
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=nothing,
+ as_sql=False,
+ template_args=template_args,
+ ):
+ script.run_env()
+
+ return script.generate_revision(
+ rev_id or util.rev_id(),
+ message,
+ refresh=True,
+ head=revisions,
+ branch_labels=branch_label,
+ **template_args, # type:ignore[arg-type]
+ )
+
+
+def upgrade(
+ config: Config,
+ revision: str,
+ sql: bool = False,
+ tag: Optional[str] = None,
+) -> None:
+ """Upgrade to a later version.
+
+ :param config: a :class:`.Config` instance.
+
+ :param revision: string revision target or range for --sql mode
+
+ :param sql: if True, use ``--sql`` mode
+
+ :param tag: an arbitrary "tag" that can be intercepted by custom
+ ``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument`
+ method.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ starting_rev = None
+ if ":" in revision:
+ if not sql:
+ raise util.CommandError("Range revision not allowed")
+ starting_rev, revision = revision.split(":", 2)
+
+ def upgrade(rev, context):
+ return script._upgrade_revs(revision, rev)
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=upgrade,
+ as_sql=sql,
+ starting_rev=starting_rev,
+ destination_rev=revision,
+ tag=tag,
+ ):
+ script.run_env()
+
+
+def downgrade(
+ config: Config,
+ revision: str,
+ sql: bool = False,
+ tag: Optional[str] = None,
+) -> None:
+ """Revert to a previous version.
+
+ :param config: a :class:`.Config` instance.
+
+ :param revision: string revision target or range for --sql mode
+
+ :param sql: if True, use ``--sql`` mode
+
+ :param tag: an arbitrary "tag" that can be intercepted by custom
+ ``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument`
+ method.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+ starting_rev = None
+ if ":" in revision:
+ if not sql:
+ raise util.CommandError("Range revision not allowed")
+ starting_rev, revision = revision.split(":", 2)
+ elif sql:
+ raise util.CommandError(
+ "downgrade with --sql requires :"
+ )
+
+ def downgrade(rev, context):
+ return script._downgrade_revs(revision, rev)
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=downgrade,
+ as_sql=sql,
+ starting_rev=starting_rev,
+ destination_rev=revision,
+ tag=tag,
+ ):
+ script.run_env()
+
+
+def show(config, rev):
+ """Show the revision(s) denoted by the given symbol.
+
+ :param config: a :class:`.Config` instance.
+
+ :param revision: string revision target
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ if rev == "current":
+
+ def show_current(rev, context):
+ for sc in script.get_revisions(rev):
+ config.print_stdout(sc.log_entry)
+ return []
+
+ with EnvironmentContext(config, script, fn=show_current):
+ script.run_env()
+ else:
+ for sc in script.get_revisions(rev):
+ config.print_stdout(sc.log_entry)
+
+
+def history(
+ config: Config,
+ rev_range: Optional[str] = None,
+ verbose: bool = False,
+ indicate_current: bool = False,
+) -> None:
+ """List changeset scripts in chronological order.
+
+ :param config: a :class:`.Config` instance.
+
+ :param rev_range: string revision range
+
+ :param verbose: output in verbose mode.
+
+ :param indicate_current: indicate current revision.
+
+ """
+ base: Optional[str]
+ head: Optional[str]
+ script = ScriptDirectory.from_config(config)
+ if rev_range is not None:
+ if ":" not in rev_range:
+ raise util.CommandError(
+ "History range requires [start]:[end], " "[start]:, or :[end]"
+ )
+ base, head = rev_range.strip().split(":")
+ else:
+ base = head = None
+
+ environment = (
+ util.asbool(config.get_main_option("revision_environment"))
+ or indicate_current
+ )
+
+ def _display_history(config, script, base, head, currents=()):
+ for sc in script.walk_revisions(
+ base=base or "base", head=head or "heads"
+ ):
+ if indicate_current:
+ sc._db_current_indicator = sc.revision in currents
+
+ config.print_stdout(
+ sc.cmd_format(
+ verbose=verbose,
+ include_branches=True,
+ include_doc=True,
+ include_parents=True,
+ )
+ )
+
+ def _display_history_w_current(config, script, base, head):
+ def _display_current_history(rev, context):
+ if head == "current":
+ _display_history(config, script, base, rev, rev)
+ elif base == "current":
+ _display_history(config, script, rev, head, rev)
+ else:
+ _display_history(config, script, base, head, rev)
+ return []
+
+ with EnvironmentContext(config, script, fn=_display_current_history):
+ script.run_env()
+
+ if base == "current" or head == "current" or environment:
+ _display_history_w_current(config, script, base, head)
+ else:
+ _display_history(config, script, base, head)
+
+
+def heads(config, verbose=False, resolve_dependencies=False):
+ """Show current available heads in the script directory.
+
+ :param config: a :class:`.Config` instance.
+
+ :param verbose: output in verbose mode.
+
+ :param resolve_dependencies: treat dependency version as down revisions.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+ if resolve_dependencies:
+ heads = script.get_revisions("heads")
+ else:
+ heads = script.get_revisions(script.get_heads())
+
+ for rev in heads:
+ config.print_stdout(
+ rev.cmd_format(
+ verbose, include_branches=True, tree_indicators=False
+ )
+ )
+
+
+def branches(config, verbose=False):
+ """Show current branch points.
+
+ :param config: a :class:`.Config` instance.
+
+ :param verbose: output in verbose mode.
+
+ """
+ script = ScriptDirectory.from_config(config)
+ for sc in script.walk_revisions():
+ if sc.is_branch_point:
+ config.print_stdout(
+ "%s\n%s\n",
+ sc.cmd_format(verbose, include_branches=True),
+ "\n".join(
+ "%s -> %s"
+ % (
+ " " * len(str(sc.revision)),
+ rev_obj.cmd_format(
+ False, include_branches=True, include_doc=verbose
+ ),
+ )
+ for rev_obj in (
+ script.get_revision(rev) for rev in sc.nextrev
+ )
+ ),
+ )
+
+
+def current(config: Config, verbose: bool = False) -> None:
+ """Display the current revision for a database.
+
+ :param config: a :class:`.Config` instance.
+
+ :param verbose: output in verbose mode.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ def display_version(rev, context):
+ if verbose:
+ config.print_stdout(
+ "Current revision(s) for %s:",
+ util.obfuscate_url_pw(context.connection.engine.url),
+ )
+ for rev in script.get_all_current(rev):
+ config.print_stdout(rev.cmd_format(verbose))
+
+ return []
+
+ with EnvironmentContext(
+ config, script, fn=display_version, dont_mutate=True
+ ):
+ script.run_env()
+
+
+def stamp(
+ config: Config,
+ revision: _RevIdType,
+ sql: bool = False,
+ tag: Optional[str] = None,
+ purge: bool = False,
+) -> None:
+ """'stamp' the revision table with the given revision; don't
+ run any migrations.
+
+ :param config: a :class:`.Config` instance.
+
+ :param revision: target revision or list of revisions. May be a list
+ to indicate stamping of multiple branch heads.
+
+ .. note:: this parameter is called "revisions" in the command line
+ interface.
+
+ :param sql: use ``--sql`` mode
+
+ :param tag: an arbitrary "tag" that can be intercepted by custom
+ ``env.py`` scripts via the :class:`.EnvironmentContext.get_tag_argument`
+ method.
+
+ :param purge: delete all entries in the version table before stamping.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ if sql:
+ destination_revs = []
+ starting_rev = None
+ for _revision in util.to_list(revision):
+ if ":" in _revision:
+ srev, _revision = _revision.split(":", 2)
+
+ if starting_rev != srev:
+ if starting_rev is None:
+ starting_rev = srev
+ else:
+ raise util.CommandError(
+ "Stamp operation with --sql only supports a "
+ "single starting revision at a time"
+ )
+ destination_revs.append(_revision)
+ else:
+ destination_revs = util.to_list(revision)
+
+ def do_stamp(rev, context):
+ return script._stamp_revs(util.to_tuple(destination_revs), rev)
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=do_stamp,
+ as_sql=sql,
+ starting_rev=starting_rev if sql else None,
+ destination_rev=util.to_tuple(destination_revs),
+ tag=tag,
+ purge=purge,
+ ):
+ script.run_env()
+
+
+def edit(config: Config, rev: str) -> None:
+ """Edit revision script(s) using $EDITOR.
+
+ :param config: a :class:`.Config` instance.
+
+ :param rev: target revision.
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ if rev == "current":
+
+ def edit_current(rev, context):
+ if not rev:
+ raise util.CommandError("No current revisions")
+ for sc in script.get_revisions(rev):
+ util.open_in_editor(sc.path)
+ return []
+
+ with EnvironmentContext(config, script, fn=edit_current):
+ script.run_env()
+ else:
+ revs = script.get_revisions(rev)
+ if not revs:
+ raise util.CommandError(
+ "No revision files indicated by symbol '%s'" % rev
+ )
+ for sc in revs:
+ assert sc
+ util.open_in_editor(sc.path)
+
+
+def ensure_version(config: Config, sql: bool = False) -> None:
+ """Create the alembic version table if it doesn't exist already .
+
+ :param config: a :class:`.Config` instance.
+
+ :param sql: use ``--sql`` mode
+
+ .. versionadded:: 1.7.6
+
+ """
+
+ script = ScriptDirectory.from_config(config)
+
+ def do_ensure_version(rev, context):
+ context._ensure_version_table()
+ return []
+
+ with EnvironmentContext(
+ config,
+ script,
+ fn=do_ensure_version,
+ as_sql=sql,
+ ):
+ script.run_env()
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/config.py b/backend/venv/lib/python3.9/site-packages/alembic/config.py
new file mode 100644
index 00000000..4b2263fd
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/config.py
@@ -0,0 +1,645 @@
+from __future__ import annotations
+
+from argparse import ArgumentParser
+from argparse import Namespace
+from configparser import ConfigParser
+import inspect
+import os
+import sys
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import Mapping
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import TextIO
+from typing import Union
+
+from typing_extensions import TypedDict
+
+from . import __version__
+from . import command
+from . import util
+from .util import compat
+
+
+class Config:
+ r"""Represent an Alembic configuration.
+
+ Within an ``env.py`` script, this is available
+ via the :attr:`.EnvironmentContext.config` attribute,
+ which in turn is available at ``alembic.context``::
+
+ from alembic import context
+
+ some_param = context.config.get_main_option("my option")
+
+ When invoking Alembic programmatically, a new
+ :class:`.Config` can be created by passing
+ the name of an .ini file to the constructor::
+
+ from alembic.config import Config
+ alembic_cfg = Config("/path/to/yourapp/alembic.ini")
+
+ With a :class:`.Config` object, you can then
+ run Alembic commands programmatically using the directives
+ in :mod:`alembic.command`.
+
+ The :class:`.Config` object can also be constructed without
+ a filename. Values can be set programmatically, and
+ new sections will be created as needed::
+
+ from alembic.config import Config
+ alembic_cfg = Config()
+ alembic_cfg.set_main_option("script_location", "myapp:migrations")
+ alembic_cfg.set_main_option("sqlalchemy.url", "postgresql://foo/bar")
+ alembic_cfg.set_section_option("mysection", "foo", "bar")
+
+ .. warning::
+
+ When using programmatic configuration, make sure the
+ ``env.py`` file in use is compatible with the target configuration;
+ including that the call to Python ``logging.fileConfig()`` is
+ omitted if the programmatic configuration doesn't actually include
+ logging directives.
+
+ For passing non-string values to environments, such as connections and
+ engines, use the :attr:`.Config.attributes` dictionary::
+
+ with engine.begin() as connection:
+ alembic_cfg.attributes['connection'] = connection
+ command.upgrade(alembic_cfg, "head")
+
+ :param file\_: name of the .ini file to open.
+ :param ini_section: name of the main Alembic section within the
+ .ini file
+ :param output_buffer: optional file-like input buffer which
+ will be passed to the :class:`.MigrationContext` - used to redirect
+ the output of "offline generation" when using Alembic programmatically.
+ :param stdout: buffer where the "print" output of commands will be sent.
+ Defaults to ``sys.stdout``.
+
+ :param config_args: A dictionary of keys and values that will be used
+ for substitution in the alembic config file. The dictionary as given
+ is **copied** to a new one, stored locally as the attribute
+ ``.config_args``. When the :attr:`.Config.file_config` attribute is
+ first invoked, the replacement variable ``here`` will be added to this
+ dictionary before the dictionary is passed to ``ConfigParser()``
+ to parse the .ini file.
+
+ :param attributes: optional dictionary of arbitrary Python keys/values,
+ which will be populated into the :attr:`.Config.attributes` dictionary.
+
+ .. seealso::
+
+ :ref:`connection_sharing`
+
+ """
+
+ def __init__(
+ self,
+ file_: Union[str, os.PathLike[str], None] = None,
+ ini_section: str = "alembic",
+ output_buffer: Optional[TextIO] = None,
+ stdout: TextIO = sys.stdout,
+ cmd_opts: Optional[Namespace] = None,
+ config_args: Mapping[str, Any] = util.immutabledict(),
+ attributes: Optional[Dict[str, Any]] = None,
+ ) -> None:
+ """Construct a new :class:`.Config`"""
+ self.config_file_name = file_
+ self.config_ini_section = ini_section
+ self.output_buffer = output_buffer
+ self.stdout = stdout
+ self.cmd_opts = cmd_opts
+ self.config_args = dict(config_args)
+ if attributes:
+ self.attributes.update(attributes)
+
+ cmd_opts: Optional[Namespace] = None
+ """The command-line options passed to the ``alembic`` script.
+
+ Within an ``env.py`` script this can be accessed via the
+ :attr:`.EnvironmentContext.config` attribute.
+
+ .. seealso::
+
+ :meth:`.EnvironmentContext.get_x_argument`
+
+ """
+
+ config_file_name: Union[str, os.PathLike[str], None] = None
+ """Filesystem path to the .ini file in use."""
+
+ config_ini_section: str = None # type:ignore[assignment]
+ """Name of the config file section to read basic configuration
+ from. Defaults to ``alembic``, that is the ``[alembic]`` section
+ of the .ini file. This value is modified using the ``-n/--name``
+ option to the Alembic runner.
+
+ """
+
+ @util.memoized_property
+ def attributes(self) -> Dict[str, Any]:
+ """A Python dictionary for storage of additional state.
+
+
+ This is a utility dictionary which can include not just strings but
+ engines, connections, schema objects, or anything else.
+ Use this to pass objects into an env.py script, such as passing
+ a :class:`sqlalchemy.engine.base.Connection` when calling
+ commands from :mod:`alembic.command` programmatically.
+
+ .. seealso::
+
+ :ref:`connection_sharing`
+
+ :paramref:`.Config.attributes`
+
+ """
+ return {}
+
+ def print_stdout(self, text: str, *arg: Any) -> None:
+ """Render a message to standard out.
+
+ When :meth:`.Config.print_stdout` is called with additional args
+ those arguments will formatted against the provided text,
+ otherwise we simply output the provided text verbatim.
+
+ This is a no-op when the``quiet`` messaging option is enabled.
+
+ e.g.::
+
+ >>> config.print_stdout('Some text %s', 'arg')
+ Some Text arg
+
+ """
+
+ if arg:
+ output = str(text) % arg
+ else:
+ output = str(text)
+
+ util.write_outstream(self.stdout, output, "\n", **self.messaging_opts)
+
+ @util.memoized_property
+ def file_config(self) -> ConfigParser:
+ """Return the underlying ``ConfigParser`` object.
+
+ Direct access to the .ini file is available here,
+ though the :meth:`.Config.get_section` and
+ :meth:`.Config.get_main_option`
+ methods provide a possibly simpler interface.
+
+ """
+
+ if self.config_file_name:
+ here = os.path.abspath(os.path.dirname(self.config_file_name))
+ else:
+ here = ""
+ self.config_args["here"] = here
+ file_config = ConfigParser(self.config_args)
+ if self.config_file_name:
+ compat.read_config_parser(file_config, [self.config_file_name])
+ else:
+ file_config.add_section(self.config_ini_section)
+ return file_config
+
+ def get_template_directory(self) -> str:
+ """Return the directory where Alembic setup templates are found.
+
+ This method is used by the alembic ``init`` and ``list_templates``
+ commands.
+
+ """
+ import alembic
+
+ package_dir = os.path.abspath(os.path.dirname(alembic.__file__))
+ return os.path.join(package_dir, "templates")
+
+ @overload
+ def get_section(
+ self, name: str, default: None = ...
+ ) -> Optional[Dict[str, str]]:
+ ...
+
+ # "default" here could also be a TypeVar
+ # _MT = TypeVar("_MT", bound=Mapping[str, str]),
+ # however mypy wasn't handling that correctly (pyright was)
+ @overload
+ def get_section(
+ self, name: str, default: Dict[str, str]
+ ) -> Dict[str, str]:
+ ...
+
+ @overload
+ def get_section(
+ self, name: str, default: Mapping[str, str]
+ ) -> Union[Dict[str, str], Mapping[str, str]]:
+ ...
+
+ def get_section(
+ self, name: str, default: Optional[Mapping[str, str]] = None
+ ) -> Optional[Mapping[str, str]]:
+ """Return all the configuration options from a given .ini file section
+ as a dictionary.
+
+ If the given section does not exist, the value of ``default``
+ is returned, which is expected to be a dictionary or other mapping.
+
+ """
+ if not self.file_config.has_section(name):
+ return default
+
+ return dict(self.file_config.items(name))
+
+ def set_main_option(self, name: str, value: str) -> None:
+ """Set an option programmatically within the 'main' section.
+
+ This overrides whatever was in the .ini file.
+
+ :param name: name of the value
+
+ :param value: the value. Note that this value is passed to
+ ``ConfigParser.set``, which supports variable interpolation using
+ pyformat (e.g. ``%(some_value)s``). A raw percent sign not part of
+ an interpolation symbol must therefore be escaped, e.g. ``%%``.
+ The given value may refer to another value already in the file
+ using the interpolation format.
+
+ """
+ self.set_section_option(self.config_ini_section, name, value)
+
+ def remove_main_option(self, name: str) -> None:
+ self.file_config.remove_option(self.config_ini_section, name)
+
+ def set_section_option(self, section: str, name: str, value: str) -> None:
+ """Set an option programmatically within the given section.
+
+ The section is created if it doesn't exist already.
+ The value here will override whatever was in the .ini
+ file.
+
+ :param section: name of the section
+
+ :param name: name of the value
+
+ :param value: the value. Note that this value is passed to
+ ``ConfigParser.set``, which supports variable interpolation using
+ pyformat (e.g. ``%(some_value)s``). A raw percent sign not part of
+ an interpolation symbol must therefore be escaped, e.g. ``%%``.
+ The given value may refer to another value already in the file
+ using the interpolation format.
+
+ """
+
+ if not self.file_config.has_section(section):
+ self.file_config.add_section(section)
+ self.file_config.set(section, name, value)
+
+ def get_section_option(
+ self, section: str, name: str, default: Optional[str] = None
+ ) -> Optional[str]:
+ """Return an option from the given section of the .ini file."""
+ if not self.file_config.has_section(section):
+ raise util.CommandError(
+ "No config file %r found, or file has no "
+ "'[%s]' section" % (self.config_file_name, section)
+ )
+ if self.file_config.has_option(section, name):
+ return self.file_config.get(section, name)
+ else:
+ return default
+
+ @overload
+ def get_main_option(self, name: str, default: str) -> str:
+ ...
+
+ @overload
+ def get_main_option(
+ self, name: str, default: Optional[str] = None
+ ) -> Optional[str]:
+ ...
+
+ def get_main_option(
+ self, name: str, default: Optional[str] = None
+ ) -> Optional[str]:
+ """Return an option from the 'main' section of the .ini file.
+
+ This defaults to being a key from the ``[alembic]``
+ section, unless the ``-n/--name`` flag were used to
+ indicate a different section.
+
+ """
+ return self.get_section_option(self.config_ini_section, name, default)
+
+ @util.memoized_property
+ def messaging_opts(self) -> MessagingOptions:
+ """The messaging options."""
+ return cast(
+ MessagingOptions,
+ util.immutabledict(
+ {"quiet": getattr(self.cmd_opts, "quiet", False)}
+ ),
+ )
+
+
+class MessagingOptions(TypedDict, total=False):
+ quiet: bool
+
+
+class CommandLine:
+ def __init__(self, prog: Optional[str] = None) -> None:
+ self._generate_args(prog)
+
+ def _generate_args(self, prog: Optional[str]) -> None:
+ def add_options(
+ fn: Any, parser: Any, positional: Any, kwargs: Any
+ ) -> None:
+ kwargs_opts = {
+ "template": (
+ "-t",
+ "--template",
+ dict(
+ default="generic",
+ type=str,
+ help="Setup template for use with 'init'",
+ ),
+ ),
+ "message": (
+ "-m",
+ "--message",
+ dict(
+ type=str, help="Message string to use with 'revision'"
+ ),
+ ),
+ "sql": (
+ "--sql",
+ dict(
+ action="store_true",
+ help="Don't emit SQL to database - dump to "
+ "standard output/file instead. See docs on "
+ "offline mode.",
+ ),
+ ),
+ "tag": (
+ "--tag",
+ dict(
+ type=str,
+ help="Arbitrary 'tag' name - can be used by "
+ "custom env.py scripts.",
+ ),
+ ),
+ "head": (
+ "--head",
+ dict(
+ type=str,
+ help="Specify head revision or @head "
+ "to base new revision on.",
+ ),
+ ),
+ "splice": (
+ "--splice",
+ dict(
+ action="store_true",
+ help="Allow a non-head revision as the "
+ "'head' to splice onto",
+ ),
+ ),
+ "depends_on": (
+ "--depends-on",
+ dict(
+ action="append",
+ help="Specify one or more revision identifiers "
+ "which this revision should depend on.",
+ ),
+ ),
+ "rev_id": (
+ "--rev-id",
+ dict(
+ type=str,
+ help="Specify a hardcoded revision id instead of "
+ "generating one",
+ ),
+ ),
+ "version_path": (
+ "--version-path",
+ dict(
+ type=str,
+ help="Specify specific path from config for "
+ "version file",
+ ),
+ ),
+ "branch_label": (
+ "--branch-label",
+ dict(
+ type=str,
+ help="Specify a branch label to apply to the "
+ "new revision",
+ ),
+ ),
+ "verbose": (
+ "-v",
+ "--verbose",
+ dict(action="store_true", help="Use more verbose output"),
+ ),
+ "resolve_dependencies": (
+ "--resolve-dependencies",
+ dict(
+ action="store_true",
+ help="Treat dependency versions as down revisions",
+ ),
+ ),
+ "autogenerate": (
+ "--autogenerate",
+ dict(
+ action="store_true",
+ help="Populate revision script with candidate "
+ "migration operations, based on comparison "
+ "of database to model.",
+ ),
+ ),
+ "rev_range": (
+ "-r",
+ "--rev-range",
+ dict(
+ action="store",
+ help="Specify a revision range; "
+ "format is [start]:[end]",
+ ),
+ ),
+ "indicate_current": (
+ "-i",
+ "--indicate-current",
+ dict(
+ action="store_true",
+ help="Indicate the current revision",
+ ),
+ ),
+ "purge": (
+ "--purge",
+ dict(
+ action="store_true",
+ help="Unconditionally erase the version table "
+ "before stamping",
+ ),
+ ),
+ "package": (
+ "--package",
+ dict(
+ action="store_true",
+ help="Write empty __init__.py files to the "
+ "environment and version locations",
+ ),
+ ),
+ }
+ positional_help = {
+ "directory": "location of scripts directory",
+ "revision": "revision identifier",
+ "revisions": "one or more revisions, or 'heads' for all heads",
+ }
+ for arg in kwargs:
+ if arg in kwargs_opts:
+ args = kwargs_opts[arg]
+ args, kw = args[0:-1], args[-1]
+ parser.add_argument(*args, **kw)
+
+ for arg in positional:
+ if (
+ arg == "revisions"
+ or fn in positional_translations
+ and positional_translations[fn][arg] == "revisions"
+ ):
+ subparser.add_argument(
+ "revisions",
+ nargs="+",
+ help=positional_help.get("revisions"),
+ )
+ else:
+ subparser.add_argument(arg, help=positional_help.get(arg))
+
+ parser = ArgumentParser(prog=prog)
+
+ parser.add_argument(
+ "--version", action="version", version="%%(prog)s %s" % __version__
+ )
+ parser.add_argument(
+ "-c",
+ "--config",
+ type=str,
+ default=os.environ.get("ALEMBIC_CONFIG", "alembic.ini"),
+ help="Alternate config file; defaults to value of "
+ 'ALEMBIC_CONFIG environment variable, or "alembic.ini"',
+ )
+ parser.add_argument(
+ "-n",
+ "--name",
+ type=str,
+ default="alembic",
+ help="Name of section in .ini file to " "use for Alembic config",
+ )
+ parser.add_argument(
+ "-x",
+ action="append",
+ help="Additional arguments consumed by "
+ "custom env.py scripts, e.g. -x "
+ "setting1=somesetting -x setting2=somesetting",
+ )
+ parser.add_argument(
+ "--raiseerr",
+ action="store_true",
+ help="Raise a full stack trace on error",
+ )
+ parser.add_argument(
+ "-q",
+ "--quiet",
+ action="store_true",
+ help="Do not log to std output.",
+ )
+ subparsers = parser.add_subparsers()
+
+ positional_translations: Dict[Any, Any] = {
+ command.stamp: {"revision": "revisions"}
+ }
+
+ for fn in [getattr(command, n) for n in dir(command)]:
+ if (
+ inspect.isfunction(fn)
+ and fn.__name__[0] != "_"
+ and fn.__module__ == "alembic.command"
+ ):
+ spec = compat.inspect_getfullargspec(fn)
+ if spec[3] is not None:
+ positional = spec[0][1 : -len(spec[3])]
+ kwarg = spec[0][-len(spec[3]) :]
+ else:
+ positional = spec[0][1:]
+ kwarg = []
+
+ if fn in positional_translations:
+ positional = [
+ positional_translations[fn].get(name, name)
+ for name in positional
+ ]
+
+ # parse first line(s) of helptext without a line break
+ help_ = fn.__doc__
+ if help_:
+ help_text = []
+ for line in help_.split("\n"):
+ if not line.strip():
+ break
+ else:
+ help_text.append(line.strip())
+ else:
+ help_text = []
+ subparser = subparsers.add_parser(
+ fn.__name__, help=" ".join(help_text)
+ )
+ add_options(fn, subparser, positional, kwarg)
+ subparser.set_defaults(cmd=(fn, positional, kwarg))
+ self.parser = parser
+
+ def run_cmd(self, config: Config, options: Namespace) -> None:
+ fn, positional, kwarg = options.cmd
+
+ try:
+ fn(
+ config,
+ *[getattr(options, k, None) for k in positional],
+ **{k: getattr(options, k, None) for k in kwarg},
+ )
+ except util.CommandError as e:
+ if options.raiseerr:
+ raise
+ else:
+ util.err(str(e), **config.messaging_opts)
+
+ def main(self, argv: Optional[Sequence[str]] = None) -> None:
+ options = self.parser.parse_args(argv)
+ if not hasattr(options, "cmd"):
+ # see http://bugs.python.org/issue9253, argparse
+ # behavior changed incompatibly in py3.3
+ self.parser.error("too few arguments")
+ else:
+ cfg = Config(
+ file_=options.config,
+ ini_section=options.name,
+ cmd_opts=options,
+ )
+ self.run_cmd(cfg, options)
+
+
+def main(
+ argv: Optional[Sequence[str]] = None,
+ prog: Optional[str] = None,
+ **kwargs: Any,
+) -> None:
+ """The console runner function for Alembic."""
+
+ CommandLine(prog=prog).main(argv=argv)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/context.py b/backend/venv/lib/python3.9/site-packages/alembic/context.py
new file mode 100644
index 00000000..758fca87
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/context.py
@@ -0,0 +1,5 @@
+from .runtime.environment import EnvironmentContext
+
+# create proxy functions for
+# each method on the EnvironmentContext class.
+EnvironmentContext.create_module_class_proxy(globals(), locals())
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/context.pyi b/backend/venv/lib/python3.9/site-packages/alembic/context.pyi
new file mode 100644
index 00000000..80619fb2
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/context.pyi
@@ -0,0 +1,853 @@
+# ### this file stubs are generated by tools/write_pyi.py - do not edit ###
+# ### imports are manually managed
+from __future__ import annotations
+
+from typing import Any
+from typing import Callable
+from typing import Collection
+from typing import ContextManager
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Literal
+from typing import Mapping
+from typing import MutableMapping
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import TextIO
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+if TYPE_CHECKING:
+ from sqlalchemy.engine.base import Connection
+ from sqlalchemy.engine.url import URL
+ from sqlalchemy.sql import Executable
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import FetchedValue
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .autogenerate.api import AutogenContext
+ from .config import Config
+ from .operations.ops import MigrationScript
+ from .runtime.migration import _ProxyTransaction
+ from .runtime.migration import MigrationContext
+ from .runtime.migration import MigrationInfo
+ from .script import ScriptDirectory
+
+### end imports ###
+
+def begin_transaction() -> Union[_ProxyTransaction, ContextManager[None]]:
+ """Return a context manager that will
+ enclose an operation within a "transaction",
+ as defined by the environment's offline
+ and transactional DDL settings.
+
+ e.g.::
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+ :meth:`.begin_transaction` is intended to
+ "do the right thing" regardless of
+ calling context:
+
+ * If :meth:`.is_transactional_ddl` is ``False``,
+ returns a "do nothing" context manager
+ which otherwise produces no transactional
+ state or directives.
+ * If :meth:`.is_offline_mode` is ``True``,
+ returns a context manager that will
+ invoke the :meth:`.DefaultImpl.emit_begin`
+ and :meth:`.DefaultImpl.emit_commit`
+ methods, which will produce the string
+ directives ``BEGIN`` and ``COMMIT`` on
+ the output stream, as rendered by the
+ target backend (e.g. SQL Server would
+ emit ``BEGIN TRANSACTION``).
+ * Otherwise, calls :meth:`sqlalchemy.engine.Connection.begin`
+ on the current online connection, which
+ returns a :class:`sqlalchemy.engine.Transaction`
+ object. This object demarcates a real
+ transaction and is itself a context manager,
+ which will roll back if an exception
+ is raised.
+
+ Note that a custom ``env.py`` script which
+ has more specific transactional needs can of course
+ manipulate the :class:`~sqlalchemy.engine.Connection`
+ directly to produce transactional state in "online"
+ mode.
+
+ """
+
+config: Config
+
+def configure(
+ connection: Optional[Connection] = None,
+ url: Union[str, URL, None] = None,
+ dialect_name: Optional[str] = None,
+ dialect_opts: Optional[Dict[str, Any]] = None,
+ transactional_ddl: Optional[bool] = None,
+ transaction_per_migration: bool = False,
+ output_buffer: Optional[TextIO] = None,
+ starting_rev: Optional[str] = None,
+ tag: Optional[str] = None,
+ template_args: Optional[Dict[str, Any]] = None,
+ render_as_batch: bool = False,
+ target_metadata: Union[MetaData, Sequence[MetaData], None] = None,
+ include_name: Optional[
+ Callable[
+ [
+ Optional[str],
+ Literal[
+ "schema",
+ "table",
+ "column",
+ "index",
+ "unique_constraint",
+ "foreign_key_constraint",
+ ],
+ MutableMapping[
+ Literal[
+ "schema_name",
+ "table_name",
+ "schema_qualified_table_name",
+ ],
+ Optional[str],
+ ],
+ ],
+ bool,
+ ]
+ ] = None,
+ include_object: Optional[
+ Callable[
+ [
+ SchemaItem,
+ Optional[str],
+ Literal[
+ "schema",
+ "table",
+ "column",
+ "index",
+ "unique_constraint",
+ "foreign_key_constraint",
+ ],
+ bool,
+ Optional[SchemaItem],
+ ],
+ bool,
+ ]
+ ] = None,
+ include_schemas: bool = False,
+ process_revision_directives: Optional[
+ Callable[
+ [
+ MigrationContext,
+ Union[str, Iterable[Optional[str]], Iterable[str]],
+ List[MigrationScript],
+ ],
+ None,
+ ]
+ ] = None,
+ compare_type: Union[
+ bool,
+ Callable[
+ [
+ MigrationContext,
+ Column[Any],
+ Column[Any],
+ TypeEngine[Any],
+ TypeEngine[Any],
+ ],
+ Optional[bool],
+ ],
+ ] = True,
+ compare_server_default: Union[
+ bool,
+ Callable[
+ [
+ MigrationContext,
+ Column[Any],
+ Column[Any],
+ Optional[str],
+ Optional[FetchedValue],
+ Optional[str],
+ ],
+ Optional[bool],
+ ],
+ ] = False,
+ render_item: Optional[
+ Callable[[str, Any, AutogenContext], Union[str, Literal[False]]]
+ ] = None,
+ literal_binds: bool = False,
+ upgrade_token: str = "upgrades",
+ downgrade_token: str = "downgrades",
+ alembic_module_prefix: str = "op.",
+ sqlalchemy_module_prefix: str = "sa.",
+ user_module_prefix: Optional[str] = None,
+ on_version_apply: Optional[
+ Callable[
+ [
+ MigrationContext,
+ MigrationInfo,
+ Collection[Any],
+ Mapping[str, Any],
+ ],
+ None,
+ ]
+ ] = None,
+ **kw: Any,
+) -> None:
+ """Configure a :class:`.MigrationContext` within this
+ :class:`.EnvironmentContext` which will provide database
+ connectivity and other configuration to a series of
+ migration scripts.
+
+ Many methods on :class:`.EnvironmentContext` require that
+ this method has been called in order to function, as they
+ ultimately need to have database access or at least access
+ to the dialect in use. Those which do are documented as such.
+
+ The important thing needed by :meth:`.configure` is a
+ means to determine what kind of database dialect is in use.
+ An actual connection to that database is needed only if
+ the :class:`.MigrationContext` is to be used in
+ "online" mode.
+
+ If the :meth:`.is_offline_mode` function returns ``True``,
+ then no connection is needed here. Otherwise, the
+ ``connection`` parameter should be present as an
+ instance of :class:`sqlalchemy.engine.Connection`.
+
+ This function is typically called from the ``env.py``
+ script within a migration environment. It can be called
+ multiple times for an invocation. The most recent
+ :class:`~sqlalchemy.engine.Connection`
+ for which it was called is the one that will be operated upon
+ by the next call to :meth:`.run_migrations`.
+
+ General parameters:
+
+ :param connection: a :class:`~sqlalchemy.engine.Connection`
+ to use
+ for SQL execution in "online" mode. When present, is also
+ used to determine the type of dialect in use.
+ :param url: a string database url, or a
+ :class:`sqlalchemy.engine.url.URL` object.
+ The type of dialect to be used will be derived from this if
+ ``connection`` is not passed.
+ :param dialect_name: string name of a dialect, such as
+ "postgresql", "mssql", etc.
+ The type of dialect to be used will be derived from this if
+ ``connection`` and ``url`` are not passed.
+ :param dialect_opts: dictionary of options to be passed to dialect
+ constructor.
+ :param transactional_ddl: Force the usage of "transactional"
+ DDL on or off;
+ this otherwise defaults to whether or not the dialect in
+ use supports it.
+ :param transaction_per_migration: if True, nest each migration script
+ in a transaction rather than the full series of migrations to
+ run.
+ :param output_buffer: a file-like object that will be used
+ for textual output
+ when the ``--sql`` option is used to generate SQL scripts.
+ Defaults to
+ ``sys.stdout`` if not passed here and also not present on
+ the :class:`.Config`
+ object. The value here overrides that of the :class:`.Config`
+ object.
+ :param output_encoding: when using ``--sql`` to generate SQL
+ scripts, apply this encoding to the string output.
+ :param literal_binds: when using ``--sql`` to generate SQL
+ scripts, pass through the ``literal_binds`` flag to the compiler
+ so that any literal values that would ordinarily be bound
+ parameters are converted to plain strings.
+
+ .. warning:: Dialects can typically only handle simple datatypes
+ like strings and numbers for auto-literal generation. Datatypes
+ like dates, intervals, and others may still require manual
+ formatting, typically using :meth:`.Operations.inline_literal`.
+
+ .. note:: the ``literal_binds`` flag is ignored on SQLAlchemy
+ versions prior to 0.8 where this feature is not supported.
+
+ .. seealso::
+
+ :meth:`.Operations.inline_literal`
+
+ :param starting_rev: Override the "starting revision" argument
+ when using ``--sql`` mode.
+ :param tag: a string tag for usage by custom ``env.py`` scripts.
+ Set via the ``--tag`` option, can be overridden here.
+ :param template_args: dictionary of template arguments which
+ will be added to the template argument environment when
+ running the "revision" command. Note that the script environment
+ is only run within the "revision" command if the --autogenerate
+ option is used, or if the option "revision_environment=true"
+ is present in the alembic.ini file.
+
+ :param version_table: The name of the Alembic version table.
+ The default is ``'alembic_version'``.
+ :param version_table_schema: Optional schema to place version
+ table within.
+ :param version_table_pk: boolean, whether the Alembic version table
+ should use a primary key constraint for the "value" column; this
+ only takes effect when the table is first created.
+ Defaults to True; setting to False should not be necessary and is
+ here for backwards compatibility reasons.
+ :param on_version_apply: a callable or collection of callables to be
+ run for each migration step.
+ The callables will be run in the order they are given, once for
+ each migration step, after the respective operation has been
+ applied but before its transaction is finalized.
+ Each callable accepts no positional arguments and the following
+ keyword arguments:
+
+ * ``ctx``: the :class:`.MigrationContext` running the migration,
+ * ``step``: a :class:`.MigrationInfo` representing the
+ step currently being applied,
+ * ``heads``: a collection of version strings representing the
+ current heads,
+ * ``run_args``: the ``**kwargs`` passed to :meth:`.run_migrations`.
+
+ Parameters specific to the autogenerate feature, when
+ ``alembic revision`` is run with the ``--autogenerate`` feature:
+
+ :param target_metadata: a :class:`sqlalchemy.schema.MetaData`
+ object, or a sequence of :class:`~sqlalchemy.schema.MetaData`
+ objects, that will be consulted during autogeneration.
+ The tables present in each :class:`~sqlalchemy.schema.MetaData`
+ will be compared against
+ what is locally available on the target
+ :class:`~sqlalchemy.engine.Connection`
+ to produce candidate upgrade/downgrade operations.
+ :param compare_type: Indicates type comparison behavior during
+ an autogenerate
+ operation. Defaults to ``True`` turning on type comparison, which
+ has good accuracy on most backends. See :ref:`compare_types`
+ for an example as well as information on other type
+ comparison options. Set to ``False`` which disables type
+ comparison. A callable can also be passed to provide custom type
+ comparison, see :ref:`compare_types` for additional details.
+
+ .. versionchanged:: 1.12.0 The default value of
+ :paramref:`.EnvironmentContext.configure.compare_type` has been
+ changed to ``True``.
+
+ .. seealso::
+
+ :ref:`compare_types`
+
+ :paramref:`.EnvironmentContext.configure.compare_server_default`
+
+ :param compare_server_default: Indicates server default comparison
+ behavior during
+ an autogenerate operation. Defaults to ``False`` which disables
+ server default
+ comparison. Set to ``True`` to turn on server default comparison,
+ which has
+ varied accuracy depending on backend.
+
+ To customize server default comparison behavior, a callable may
+ be specified
+ which can filter server default comparisons during an
+ autogenerate operation.
+ defaults during an autogenerate operation. The format of this
+ callable is::
+
+ def my_compare_server_default(context, inspected_column,
+ metadata_column, inspected_default, metadata_default,
+ rendered_metadata_default):
+ # return True if the defaults are different,
+ # False if not, or None to allow the default implementation
+ # to compare these defaults
+ return None
+
+ context.configure(
+ # ...
+ compare_server_default = my_compare_server_default
+ )
+
+ ``inspected_column`` is a dictionary structure as returned by
+ :meth:`sqlalchemy.engine.reflection.Inspector.get_columns`, whereas
+ ``metadata_column`` is a :class:`sqlalchemy.schema.Column` from
+ the local model environment.
+
+ A return value of ``None`` indicates to allow default server default
+ comparison
+ to proceed. Note that some backends such as Postgresql actually
+ execute
+ the two defaults on the database side to compare for equivalence.
+
+ .. seealso::
+
+ :paramref:`.EnvironmentContext.configure.compare_type`
+
+ :param include_name: A callable function which is given
+ the chance to return ``True`` or ``False`` for any database reflected
+ object based on its name, including database schema names when
+ the :paramref:`.EnvironmentContext.configure.include_schemas` flag
+ is set to ``True``.
+
+ The function accepts the following positional arguments:
+
+ * ``name``: the name of the object, such as schema name or table name.
+ Will be ``None`` when indicating the default schema name of the
+ database connection.
+ * ``type``: a string describing the type of object; currently
+ ``"schema"``, ``"table"``, ``"column"``, ``"index"``,
+ ``"unique_constraint"``, or ``"foreign_key_constraint"``
+ * ``parent_names``: a dictionary of "parent" object names, that are
+ relative to the name being given. Keys in this dictionary may
+ include: ``"schema_name"``, ``"table_name"`` or
+ ``"schema_qualified_table_name"``.
+
+ E.g.::
+
+ def include_name(name, type_, parent_names):
+ if type_ == "schema":
+ return name in ["schema_one", "schema_two"]
+ else:
+ return True
+
+ context.configure(
+ # ...
+ include_schemas = True,
+ include_name = include_name
+ )
+
+ .. seealso::
+
+ :ref:`autogenerate_include_hooks`
+
+ :paramref:`.EnvironmentContext.configure.include_object`
+
+ :paramref:`.EnvironmentContext.configure.include_schemas`
+
+
+ :param include_object: A callable function which is given
+ the chance to return ``True`` or ``False`` for any object,
+ indicating if the given object should be considered in the
+ autogenerate sweep.
+
+ The function accepts the following positional arguments:
+
+ * ``object``: a :class:`~sqlalchemy.schema.SchemaItem` object such
+ as a :class:`~sqlalchemy.schema.Table`,
+ :class:`~sqlalchemy.schema.Column`,
+ :class:`~sqlalchemy.schema.Index`
+ :class:`~sqlalchemy.schema.UniqueConstraint`,
+ or :class:`~sqlalchemy.schema.ForeignKeyConstraint` object
+ * ``name``: the name of the object. This is typically available
+ via ``object.name``.
+ * ``type``: a string describing the type of object; currently
+ ``"table"``, ``"column"``, ``"index"``, ``"unique_constraint"``,
+ or ``"foreign_key_constraint"``
+ * ``reflected``: ``True`` if the given object was produced based on
+ table reflection, ``False`` if it's from a local :class:`.MetaData`
+ object.
+ * ``compare_to``: the object being compared against, if available,
+ else ``None``.
+
+ E.g.::
+
+ def include_object(object, name, type_, reflected, compare_to):
+ if (type_ == "column" and
+ not reflected and
+ object.info.get("skip_autogenerate", False)):
+ return False
+ else:
+ return True
+
+ context.configure(
+ # ...
+ include_object = include_object
+ )
+
+ For the use case of omitting specific schemas from a target database
+ when :paramref:`.EnvironmentContext.configure.include_schemas` is
+ set to ``True``, the :attr:`~sqlalchemy.schema.Table.schema`
+ attribute can be checked for each :class:`~sqlalchemy.schema.Table`
+ object passed to the hook, however it is much more efficient
+ to filter on schemas before reflection of objects takes place
+ using the :paramref:`.EnvironmentContext.configure.include_name`
+ hook.
+
+ .. seealso::
+
+ :ref:`autogenerate_include_hooks`
+
+ :paramref:`.EnvironmentContext.configure.include_name`
+
+ :paramref:`.EnvironmentContext.configure.include_schemas`
+
+ :param render_as_batch: if True, commands which alter elements
+ within a table will be placed under a ``with batch_alter_table():``
+ directive, so that batch migrations will take place.
+
+ .. seealso::
+
+ :ref:`batch_migrations`
+
+ :param include_schemas: If True, autogenerate will scan across
+ all schemas located by the SQLAlchemy
+ :meth:`~sqlalchemy.engine.reflection.Inspector.get_schema_names`
+ method, and include all differences in tables found across all
+ those schemas. When using this option, you may want to also
+ use the :paramref:`.EnvironmentContext.configure.include_name`
+ parameter to specify a callable which
+ can filter the tables/schemas that get included.
+
+ .. seealso::
+
+ :ref:`autogenerate_include_hooks`
+
+ :paramref:`.EnvironmentContext.configure.include_name`
+
+ :paramref:`.EnvironmentContext.configure.include_object`
+
+ :param render_item: Callable that can be used to override how
+ any schema item, i.e. column, constraint, type,
+ etc., is rendered for autogenerate. The callable receives a
+ string describing the type of object, the object, and
+ the autogen context. If it returns False, the
+ default rendering method will be used. If it returns None,
+ the item will not be rendered in the context of a Table
+ construct, that is, can be used to skip columns or constraints
+ within op.create_table()::
+
+ def my_render_column(type_, col, autogen_context):
+ if type_ == "column" and isinstance(col, MySpecialCol):
+ return repr(col)
+ else:
+ return False
+
+ context.configure(
+ # ...
+ render_item = my_render_column
+ )
+
+ Available values for the type string include: ``"column"``,
+ ``"primary_key"``, ``"foreign_key"``, ``"unique"``, ``"check"``,
+ ``"type"``, ``"server_default"``.
+
+ .. seealso::
+
+ :ref:`autogen_render_types`
+
+ :param upgrade_token: When autogenerate completes, the text of the
+ candidate upgrade operations will be present in this template
+ variable when ``script.py.mako`` is rendered. Defaults to
+ ``upgrades``.
+ :param downgrade_token: When autogenerate completes, the text of the
+ candidate downgrade operations will be present in this
+ template variable when ``script.py.mako`` is rendered. Defaults to
+ ``downgrades``.
+
+ :param alembic_module_prefix: When autogenerate refers to Alembic
+ :mod:`alembic.operations` constructs, this prefix will be used
+ (i.e. ``op.create_table``) Defaults to "``op.``".
+ Can be ``None`` to indicate no prefix.
+
+ :param sqlalchemy_module_prefix: When autogenerate refers to
+ SQLAlchemy
+ :class:`~sqlalchemy.schema.Column` or type classes, this prefix
+ will be used
+ (i.e. ``sa.Column("somename", sa.Integer)``) Defaults to "``sa.``".
+ Can be ``None`` to indicate no prefix.
+ Note that when dialect-specific types are rendered, autogenerate
+ will render them using the dialect module name, i.e. ``mssql.BIT()``,
+ ``postgresql.UUID()``.
+
+ :param user_module_prefix: When autogenerate refers to a SQLAlchemy
+ type (e.g. :class:`.TypeEngine`) where the module name is not
+ under the ``sqlalchemy`` namespace, this prefix will be used
+ within autogenerate. If left at its default of
+ ``None``, the ``__module__`` attribute of the type is used to
+ render the import module. It's a good practice to set this
+ and to have all custom types be available from a fixed module space,
+ in order to future-proof migration files against reorganizations
+ in modules.
+
+ .. seealso::
+
+ :ref:`autogen_module_prefix`
+
+ :param process_revision_directives: a callable function that will
+ be passed a structure representing the end result of an autogenerate
+ or plain "revision" operation, which can be manipulated to affect
+ how the ``alembic revision`` command ultimately outputs new
+ revision scripts. The structure of the callable is::
+
+ def process_revision_directives(context, revision, directives):
+ pass
+
+ The ``directives`` parameter is a Python list containing
+ a single :class:`.MigrationScript` directive, which represents
+ the revision file to be generated. This list as well as its
+ contents may be freely modified to produce any set of commands.
+ The section :ref:`customizing_revision` shows an example of
+ doing this. The ``context`` parameter is the
+ :class:`.MigrationContext` in use,
+ and ``revision`` is a tuple of revision identifiers representing the
+ current revision of the database.
+
+ The callable is invoked at all times when the ``--autogenerate``
+ option is passed to ``alembic revision``. If ``--autogenerate``
+ is not passed, the callable is invoked only if the
+ ``revision_environment`` variable is set to True in the Alembic
+ configuration, in which case the given ``directives`` collection
+ will contain empty :class:`.UpgradeOps` and :class:`.DowngradeOps`
+ collections for ``.upgrade_ops`` and ``.downgrade_ops``. The
+ ``--autogenerate`` option itself can be inferred by inspecting
+ ``context.config.cmd_opts.autogenerate``.
+
+ The callable function may optionally be an instance of
+ a :class:`.Rewriter` object. This is a helper object that
+ assists in the production of autogenerate-stream rewriter functions.
+
+ .. seealso::
+
+ :ref:`customizing_revision`
+
+ :ref:`autogen_rewriter`
+
+ :paramref:`.command.revision.process_revision_directives`
+
+ Parameters specific to individual backends:
+
+ :param mssql_batch_separator: The "batch separator" which will
+ be placed between each statement when generating offline SQL Server
+ migrations. Defaults to ``GO``. Note this is in addition to the
+ customary semicolon ``;`` at the end of each statement; SQL Server
+ considers the "batch separator" to denote the end of an
+ individual statement execution, and cannot group certain
+ dependent operations in one step.
+ :param oracle_batch_separator: The "batch separator" which will
+ be placed between each statement when generating offline
+ Oracle migrations. Defaults to ``/``. Oracle doesn't add a
+ semicolon between statements like most other backends.
+
+ """
+
+def execute(
+ sql: Union[Executable, str],
+ execution_options: Optional[Dict[str, Any]] = None,
+) -> None:
+ """Execute the given SQL using the current change context.
+
+ The behavior of :meth:`.execute` is the same
+ as that of :meth:`.Operations.execute`. Please see that
+ function's documentation for full detail including
+ caveats and limitations.
+
+ This function requires that a :class:`.MigrationContext` has
+ first been made available via :meth:`.configure`.
+
+ """
+
+def get_bind() -> Connection:
+ """Return the current 'bind'.
+
+ In "online" mode, this is the
+ :class:`sqlalchemy.engine.Connection` currently being used
+ to emit SQL to the database.
+
+ This function requires that a :class:`.MigrationContext`
+ has first been made available via :meth:`.configure`.
+
+ """
+
+def get_context() -> MigrationContext:
+ """Return the current :class:`.MigrationContext` object.
+
+ If :meth:`.EnvironmentContext.configure` has not been
+ called yet, raises an exception.
+
+ """
+
+def get_head_revision() -> Union[str, Tuple[str, ...], None]:
+ """Return the hex identifier of the 'head' script revision.
+
+ If the script directory has multiple heads, this
+ method raises a :class:`.CommandError`;
+ :meth:`.EnvironmentContext.get_head_revisions` should be preferred.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ .. seealso:: :meth:`.EnvironmentContext.get_head_revisions`
+
+ """
+
+def get_head_revisions() -> Union[str, Tuple[str, ...], None]:
+ """Return the hex identifier of the 'heads' script revision(s).
+
+ This returns a tuple containing the version number of all
+ heads in the script directory.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+
+def get_revision_argument() -> Union[str, Tuple[str, ...], None]:
+ """Get the 'destination' revision argument.
+
+ This is typically the argument passed to the
+ ``upgrade`` or ``downgrade`` command.
+
+ If it was specified as ``head``, the actual
+ version number is returned; if specified
+ as ``base``, ``None`` is returned.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+
+def get_starting_revision_argument() -> Union[str, Tuple[str, ...], None]:
+ """Return the 'starting revision' argument,
+ if the revision was passed using ``start:end``.
+
+ This is only meaningful in "offline" mode.
+ Returns ``None`` if no value is available
+ or was configured.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+
+def get_tag_argument() -> Optional[str]:
+ """Return the value passed for the ``--tag`` argument, if any.
+
+ The ``--tag`` argument is not used directly by Alembic,
+ but is available for custom ``env.py`` configurations that
+ wish to use it; particularly for offline generation scripts
+ that wish to generate tagged filenames.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ .. seealso::
+
+ :meth:`.EnvironmentContext.get_x_argument` - a newer and more
+ open ended system of extending ``env.py`` scripts via the command
+ line.
+
+ """
+
+@overload
+def get_x_argument(as_dictionary: Literal[False]) -> List[str]: ...
+@overload
+def get_x_argument(as_dictionary: Literal[True]) -> Dict[str, str]: ...
+@overload
+def get_x_argument(
+ as_dictionary: bool = ...,
+) -> Union[List[str], Dict[str, str]]:
+ """Return the value(s) passed for the ``-x`` argument, if any.
+
+ The ``-x`` argument is an open ended flag that allows any user-defined
+ value or values to be passed on the command line, then available
+ here for consumption by a custom ``env.py`` script.
+
+ The return value is a list, returned directly from the ``argparse``
+ structure. If ``as_dictionary=True`` is passed, the ``x`` arguments
+ are parsed using ``key=value`` format into a dictionary that is
+ then returned. If there is no ``=`` in the argument, value is an empty
+ string.
+
+ .. versionchanged:: 1.13.1 Support ``as_dictionary=True`` when
+ arguments are passed without the ``=`` symbol.
+
+ For example, to support passing a database URL on the command line,
+ the standard ``env.py`` script can be modified like this::
+
+ cmd_line_url = context.get_x_argument(
+ as_dictionary=True).get('dbname')
+ if cmd_line_url:
+ engine = create_engine(cmd_line_url)
+ else:
+ engine = engine_from_config(
+ config.get_section(config.config_ini_section),
+ prefix='sqlalchemy.',
+ poolclass=pool.NullPool)
+
+ This then takes effect by running the ``alembic`` script as::
+
+ alembic -x dbname=postgresql://user:pass@host/dbname upgrade head
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ .. seealso::
+
+ :meth:`.EnvironmentContext.get_tag_argument`
+
+ :attr:`.Config.cmd_opts`
+
+ """
+
+def is_offline_mode() -> bool:
+ """Return True if the current migrations environment
+ is running in "offline mode".
+
+ This is ``True`` or ``False`` depending
+ on the ``--sql`` flag passed.
+
+ This function does not require that the :class:`.MigrationContext`
+ has been configured.
+
+ """
+
+def is_transactional_ddl() -> bool:
+ """Return True if the context is configured to expect a
+ transactional DDL capable backend.
+
+ This defaults to the type of database in use, and
+ can be overridden by the ``transactional_ddl`` argument
+ to :meth:`.configure`
+
+ This function requires that a :class:`.MigrationContext`
+ has first been made available via :meth:`.configure`.
+
+ """
+
+def run_migrations(**kw: Any) -> None:
+ """Run migrations as determined by the current command line
+ configuration
+ as well as versioning information present (or not) in the current
+ database connection (if one is present).
+
+ The function accepts optional ``**kw`` arguments. If these are
+ passed, they are sent directly to the ``upgrade()`` and
+ ``downgrade()``
+ functions within each target revision file. By modifying the
+ ``script.py.mako`` file so that the ``upgrade()`` and ``downgrade()``
+ functions accept arguments, parameters can be passed here so that
+ contextual information, usually information to identify a particular
+ database in use, can be passed from a custom ``env.py`` script
+ to the migration functions.
+
+ This function requires that a :class:`.MigrationContext` has
+ first been made available via :meth:`.configure`.
+
+ """
+
+script: ScriptDirectory
+
+def static_output(text: str) -> None:
+ """Emit text directly to the "offline" SQL stream.
+
+ Typically this is for emitting comments that
+ start with --. The statement is not treated
+ as a SQL execution, no ; or batch separator
+ is added, etc.
+
+ """
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/ddl/__init__.py b/backend/venv/lib/python3.9/site-packages/alembic/ddl/__init__.py
new file mode 100644
index 00000000..f2f72b3d
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/ddl/__init__.py
@@ -0,0 +1,6 @@
+from . import mssql
+from . import mysql
+from . import oracle
+from . import postgresql
+from . import sqlite
+from .impl import DefaultImpl as DefaultImpl
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/ddl/_autogen.py b/backend/venv/lib/python3.9/site-packages/alembic/ddl/_autogen.py
new file mode 100644
index 00000000..e22153c4
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/ddl/_autogen.py
@@ -0,0 +1,325 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+from typing import Any
+from typing import ClassVar
+from typing import Dict
+from typing import Generic
+from typing import NamedTuple
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from sqlalchemy.sql.schema import Constraint
+from sqlalchemy.sql.schema import ForeignKeyConstraint
+from sqlalchemy.sql.schema import Index
+from sqlalchemy.sql.schema import UniqueConstraint
+from typing_extensions import TypeGuard
+
+from .. import util
+from ..util import sqla_compat
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from alembic.autogenerate.api import AutogenContext
+ from alembic.ddl.impl import DefaultImpl
+
+CompareConstraintType = Union[Constraint, Index]
+
+_C = TypeVar("_C", bound=CompareConstraintType)
+
+_clsreg: Dict[str, Type[_constraint_sig]] = {}
+
+
+class ComparisonResult(NamedTuple):
+ status: Literal["equal", "different", "skip"]
+ message: str
+
+ @property
+ def is_equal(self) -> bool:
+ return self.status == "equal"
+
+ @property
+ def is_different(self) -> bool:
+ return self.status == "different"
+
+ @property
+ def is_skip(self) -> bool:
+ return self.status == "skip"
+
+ @classmethod
+ def Equal(cls) -> ComparisonResult:
+ """the constraints are equal."""
+ return cls("equal", "The two constraints are equal")
+
+ @classmethod
+ def Different(cls, reason: Union[str, Sequence[str]]) -> ComparisonResult:
+ """the constraints are different for the provided reason(s)."""
+ return cls("different", ", ".join(util.to_list(reason)))
+
+ @classmethod
+ def Skip(cls, reason: Union[str, Sequence[str]]) -> ComparisonResult:
+ """the constraint cannot be compared for the provided reason(s).
+
+ The message is logged, but the constraints will be otherwise
+ considered equal, meaning that no migration command will be
+ generated.
+ """
+ return cls("skip", ", ".join(util.to_list(reason)))
+
+
+class _constraint_sig(Generic[_C]):
+ const: _C
+
+ _sig: Tuple[Any, ...]
+ name: Optional[sqla_compat._ConstraintNameDefined]
+
+ impl: DefaultImpl
+
+ _is_index: ClassVar[bool] = False
+ _is_fk: ClassVar[bool] = False
+ _is_uq: ClassVar[bool] = False
+
+ _is_metadata: bool
+
+ def __init_subclass__(cls) -> None:
+ cls._register()
+
+ @classmethod
+ def _register(cls):
+ raise NotImplementedError()
+
+ def __init__(
+ self, is_metadata: bool, impl: DefaultImpl, const: _C
+ ) -> None:
+ raise NotImplementedError()
+
+ def compare_to_reflected(
+ self, other: _constraint_sig[Any]
+ ) -> ComparisonResult:
+ assert self.impl is other.impl
+ assert self._is_metadata
+ assert not other._is_metadata
+
+ return self._compare_to_reflected(other)
+
+ def _compare_to_reflected(
+ self, other: _constraint_sig[_C]
+ ) -> ComparisonResult:
+ raise NotImplementedError()
+
+ @classmethod
+ def from_constraint(
+ cls, is_metadata: bool, impl: DefaultImpl, constraint: _C
+ ) -> _constraint_sig[_C]:
+ # these could be cached by constraint/impl, however, if the
+ # constraint is modified in place, then the sig is wrong. the mysql
+ # impl currently does this, and if we fixed that we can't be sure
+ # someone else might do it too, so play it safe.
+ sig = _clsreg[constraint.__visit_name__](is_metadata, impl, constraint)
+ return sig
+
+ def md_name_to_sql_name(self, context: AutogenContext) -> Optional[str]:
+ return sqla_compat._get_constraint_final_name(
+ self.const, context.dialect
+ )
+
+ @util.memoized_property
+ def is_named(self):
+ return sqla_compat._constraint_is_named(self.const, self.impl.dialect)
+
+ @util.memoized_property
+ def unnamed(self) -> Tuple[Any, ...]:
+ return self._sig
+
+ @util.memoized_property
+ def unnamed_no_options(self) -> Tuple[Any, ...]:
+ raise NotImplementedError()
+
+ @util.memoized_property
+ def _full_sig(self) -> Tuple[Any, ...]:
+ return (self.name,) + self.unnamed
+
+ def __eq__(self, other) -> bool:
+ return self._full_sig == other._full_sig
+
+ def __ne__(self, other) -> bool:
+ return self._full_sig != other._full_sig
+
+ def __hash__(self) -> int:
+ return hash(self._full_sig)
+
+
+class _uq_constraint_sig(_constraint_sig[UniqueConstraint]):
+ _is_uq = True
+
+ @classmethod
+ def _register(cls) -> None:
+ _clsreg["unique_constraint"] = cls
+
+ is_unique = True
+
+ def __init__(
+ self,
+ is_metadata: bool,
+ impl: DefaultImpl,
+ const: UniqueConstraint,
+ ) -> None:
+ self.impl = impl
+ self.const = const
+ self.name = sqla_compat.constraint_name_or_none(const.name)
+ self._sig = tuple(sorted([col.name for col in const.columns]))
+ self._is_metadata = is_metadata
+
+ @property
+ def column_names(self) -> Tuple[str, ...]:
+ return tuple([col.name for col in self.const.columns])
+
+ def _compare_to_reflected(
+ self, other: _constraint_sig[_C]
+ ) -> ComparisonResult:
+ assert self._is_metadata
+ metadata_obj = self
+ conn_obj = other
+
+ assert is_uq_sig(conn_obj)
+ return self.impl.compare_unique_constraint(
+ metadata_obj.const, conn_obj.const
+ )
+
+
+class _ix_constraint_sig(_constraint_sig[Index]):
+ _is_index = True
+
+ name: sqla_compat._ConstraintName
+
+ @classmethod
+ def _register(cls) -> None:
+ _clsreg["index"] = cls
+
+ def __init__(
+ self, is_metadata: bool, impl: DefaultImpl, const: Index
+ ) -> None:
+ self.impl = impl
+ self.const = const
+ self.name = const.name
+ self.is_unique = bool(const.unique)
+ self._is_metadata = is_metadata
+
+ def _compare_to_reflected(
+ self, other: _constraint_sig[_C]
+ ) -> ComparisonResult:
+ assert self._is_metadata
+ metadata_obj = self
+ conn_obj = other
+
+ assert is_index_sig(conn_obj)
+ return self.impl.compare_indexes(metadata_obj.const, conn_obj.const)
+
+ @util.memoized_property
+ def has_expressions(self):
+ return sqla_compat.is_expression_index(self.const)
+
+ @util.memoized_property
+ def column_names(self) -> Tuple[str, ...]:
+ return tuple([col.name for col in self.const.columns])
+
+ @util.memoized_property
+ def column_names_optional(self) -> Tuple[Optional[str], ...]:
+ return tuple(
+ [getattr(col, "name", None) for col in self.const.expressions]
+ )
+
+ @util.memoized_property
+ def is_named(self):
+ return True
+
+ @util.memoized_property
+ def unnamed(self):
+ return (self.is_unique,) + self.column_names_optional
+
+
+class _fk_constraint_sig(_constraint_sig[ForeignKeyConstraint]):
+ _is_fk = True
+
+ @classmethod
+ def _register(cls) -> None:
+ _clsreg["foreign_key_constraint"] = cls
+
+ def __init__(
+ self,
+ is_metadata: bool,
+ impl: DefaultImpl,
+ const: ForeignKeyConstraint,
+ ) -> None:
+ self._is_metadata = is_metadata
+
+ self.impl = impl
+ self.const = const
+
+ self.name = sqla_compat.constraint_name_or_none(const.name)
+
+ (
+ self.source_schema,
+ self.source_table,
+ self.source_columns,
+ self.target_schema,
+ self.target_table,
+ self.target_columns,
+ onupdate,
+ ondelete,
+ deferrable,
+ initially,
+ ) = sqla_compat._fk_spec(const)
+
+ self._sig: Tuple[Any, ...] = (
+ self.source_schema,
+ self.source_table,
+ tuple(self.source_columns),
+ self.target_schema,
+ self.target_table,
+ tuple(self.target_columns),
+ ) + (
+ (None if onupdate.lower() == "no action" else onupdate.lower())
+ if onupdate
+ else None,
+ (None if ondelete.lower() == "no action" else ondelete.lower())
+ if ondelete
+ else None,
+ # convert initially + deferrable into one three-state value
+ "initially_deferrable"
+ if initially and initially.lower() == "deferred"
+ else "deferrable"
+ if deferrable
+ else "not deferrable",
+ )
+
+ @util.memoized_property
+ def unnamed_no_options(self):
+ return (
+ self.source_schema,
+ self.source_table,
+ tuple(self.source_columns),
+ self.target_schema,
+ self.target_table,
+ tuple(self.target_columns),
+ )
+
+
+def is_index_sig(sig: _constraint_sig) -> TypeGuard[_ix_constraint_sig]:
+ return sig._is_index
+
+
+def is_uq_sig(sig: _constraint_sig) -> TypeGuard[_uq_constraint_sig]:
+ return sig._is_uq
+
+
+def is_fk_sig(sig: _constraint_sig) -> TypeGuard[_fk_constraint_sig]:
+ return sig._is_fk
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/ddl/base.py b/backend/venv/lib/python3.9/site-packages/alembic/ddl/base.py
new file mode 100644
index 00000000..7a85a5c1
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/ddl/base.py
@@ -0,0 +1,335 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import functools
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import exc
+from sqlalchemy import Integer
+from sqlalchemy import types as sqltypes
+from sqlalchemy.ext.compiler import compiles
+from sqlalchemy.schema import Column
+from sqlalchemy.schema import DDLElement
+from sqlalchemy.sql.elements import quoted_name
+
+from ..util.sqla_compat import _columns_for_constraint # noqa
+from ..util.sqla_compat import _find_columns # noqa
+from ..util.sqla_compat import _fk_spec # noqa
+from ..util.sqla_compat import _is_type_bound # noqa
+from ..util.sqla_compat import _table_for_constraint # noqa
+
+if TYPE_CHECKING:
+ from typing import Any
+
+ from sqlalchemy.sql.compiler import Compiled
+ from sqlalchemy.sql.compiler import DDLCompiler
+ from sqlalchemy.sql.elements import TextClause
+ from sqlalchemy.sql.functions import Function
+ from sqlalchemy.sql.schema import FetchedValue
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .impl import DefaultImpl
+ from ..util.sqla_compat import Computed
+ from ..util.sqla_compat import Identity
+
+_ServerDefault = Union["TextClause", "FetchedValue", "Function[Any]", str]
+
+
+class AlterTable(DDLElement):
+
+ """Represent an ALTER TABLE statement.
+
+ Only the string name and optional schema name of the table
+ is required, not a full Table object.
+
+ """
+
+ def __init__(
+ self,
+ table_name: str,
+ schema: Optional[Union[quoted_name, str]] = None,
+ ) -> None:
+ self.table_name = table_name
+ self.schema = schema
+
+
+class RenameTable(AlterTable):
+ def __init__(
+ self,
+ old_table_name: str,
+ new_table_name: Union[quoted_name, str],
+ schema: Optional[Union[quoted_name, str]] = None,
+ ) -> None:
+ super().__init__(old_table_name, schema=schema)
+ self.new_table_name = new_table_name
+
+
+class AlterColumn(AlterTable):
+ def __init__(
+ self,
+ name: str,
+ column_name: str,
+ schema: Optional[str] = None,
+ existing_type: Optional[TypeEngine] = None,
+ existing_nullable: Optional[bool] = None,
+ existing_server_default: Optional[_ServerDefault] = None,
+ existing_comment: Optional[str] = None,
+ ) -> None:
+ super().__init__(name, schema=schema)
+ self.column_name = column_name
+ self.existing_type = (
+ sqltypes.to_instance(existing_type)
+ if existing_type is not None
+ else None
+ )
+ self.existing_nullable = existing_nullable
+ self.existing_server_default = existing_server_default
+ self.existing_comment = existing_comment
+
+
+class ColumnNullable(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, nullable: bool, **kw
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.nullable = nullable
+
+
+class ColumnType(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, type_: TypeEngine, **kw
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.type_ = sqltypes.to_instance(type_)
+
+
+class ColumnName(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, newname: str, **kw
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.newname = newname
+
+
+class ColumnDefault(AlterColumn):
+ def __init__(
+ self,
+ name: str,
+ column_name: str,
+ default: Optional[_ServerDefault],
+ **kw,
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.default = default
+
+
+class ComputedColumnDefault(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, default: Optional[Computed], **kw
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.default = default
+
+
+class IdentityColumnDefault(AlterColumn):
+ def __init__(
+ self,
+ name: str,
+ column_name: str,
+ default: Optional[Identity],
+ impl: DefaultImpl,
+ **kw,
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.default = default
+ self.impl = impl
+
+
+class AddColumn(AlterTable):
+ def __init__(
+ self,
+ name: str,
+ column: Column[Any],
+ schema: Optional[Union[quoted_name, str]] = None,
+ ) -> None:
+ super().__init__(name, schema=schema)
+ self.column = column
+
+
+class DropColumn(AlterTable):
+ def __init__(
+ self, name: str, column: Column[Any], schema: Optional[str] = None
+ ) -> None:
+ super().__init__(name, schema=schema)
+ self.column = column
+
+
+class ColumnComment(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, comment: Optional[str], **kw
+ ) -> None:
+ super().__init__(name, column_name, **kw)
+ self.comment = comment
+
+
+@compiles(RenameTable) # type: ignore[misc]
+def visit_rename_table(
+ element: RenameTable, compiler: DDLCompiler, **kw
+) -> str:
+ return "%s RENAME TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_table_name(compiler, element.new_table_name, element.schema),
+ )
+
+
+@compiles(AddColumn) # type: ignore[misc]
+def visit_add_column(element: AddColumn, compiler: DDLCompiler, **kw) -> str:
+ return "%s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ add_column(compiler, element.column, **kw),
+ )
+
+
+@compiles(DropColumn) # type: ignore[misc]
+def visit_drop_column(element: DropColumn, compiler: DDLCompiler, **kw) -> str:
+ return "%s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ drop_column(compiler, element.column.name, **kw),
+ )
+
+
+@compiles(ColumnNullable) # type: ignore[misc]
+def visit_column_nullable(
+ element: ColumnNullable, compiler: DDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "DROP NOT NULL" if element.nullable else "SET NOT NULL",
+ )
+
+
+@compiles(ColumnType) # type: ignore[misc]
+def visit_column_type(element: ColumnType, compiler: DDLCompiler, **kw) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "TYPE %s" % format_type(compiler, element.type_),
+ )
+
+
+@compiles(ColumnName) # type: ignore[misc]
+def visit_column_name(element: ColumnName, compiler: DDLCompiler, **kw) -> str:
+ return "%s RENAME %s TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ format_column_name(compiler, element.newname),
+ )
+
+
+@compiles(ColumnDefault) # type: ignore[misc]
+def visit_column_default(
+ element: ColumnDefault, compiler: DDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "SET DEFAULT %s" % format_server_default(compiler, element.default)
+ if element.default is not None
+ else "DROP DEFAULT",
+ )
+
+
+@compiles(ComputedColumnDefault) # type: ignore[misc]
+def visit_computed_column(
+ element: ComputedColumnDefault, compiler: DDLCompiler, **kw
+):
+ raise exc.CompileError(
+ 'Adding or removing a "computed" construct, e.g. GENERATED '
+ "ALWAYS AS, to or from an existing column is not supported."
+ )
+
+
+@compiles(IdentityColumnDefault) # type: ignore[misc]
+def visit_identity_column(
+ element: IdentityColumnDefault, compiler: DDLCompiler, **kw
+):
+ raise exc.CompileError(
+ 'Adding, removing or modifying an "identity" construct, '
+ "e.g. GENERATED AS IDENTITY, to or from an existing "
+ "column is not supported in this dialect."
+ )
+
+
+def quote_dotted(
+ name: Union[quoted_name, str], quote: functools.partial
+) -> Union[quoted_name, str]:
+ """quote the elements of a dotted name"""
+
+ if isinstance(name, quoted_name):
+ return quote(name)
+ result = ".".join([quote(x) for x in name.split(".")])
+ return result
+
+
+def format_table_name(
+ compiler: Compiled,
+ name: Union[quoted_name, str],
+ schema: Optional[Union[quoted_name, str]],
+) -> Union[quoted_name, str]:
+ quote = functools.partial(compiler.preparer.quote)
+ if schema:
+ return quote_dotted(schema, quote) + "." + quote(name)
+ else:
+ return quote(name)
+
+
+def format_column_name(
+ compiler: DDLCompiler, name: Optional[Union[quoted_name, str]]
+) -> Union[quoted_name, str]:
+ return compiler.preparer.quote(name) # type: ignore[arg-type]
+
+
+def format_server_default(
+ compiler: DDLCompiler,
+ default: Optional[_ServerDefault],
+) -> str:
+ return compiler.get_column_default_string(
+ Column("x", Integer, server_default=default)
+ )
+
+
+def format_type(compiler: DDLCompiler, type_: TypeEngine) -> str:
+ return compiler.dialect.type_compiler.process(type_)
+
+
+def alter_table(
+ compiler: DDLCompiler,
+ name: str,
+ schema: Optional[str],
+) -> str:
+ return "ALTER TABLE %s" % format_table_name(compiler, name, schema)
+
+
+def drop_column(compiler: DDLCompiler, name: str, **kw) -> str:
+ return "DROP COLUMN %s" % format_column_name(compiler, name)
+
+
+def alter_column(compiler: DDLCompiler, name: str) -> str:
+ return "ALTER COLUMN %s" % format_column_name(compiler, name)
+
+
+def add_column(compiler: DDLCompiler, column: Column[Any], **kw) -> str:
+ text = "ADD COLUMN %s" % compiler.get_column_specification(column, **kw)
+
+ const = " ".join(
+ compiler.process(constraint) for constraint in column.constraints
+ )
+ if const:
+ text += " " + const
+
+ return text
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/ddl/impl.py b/backend/venv/lib/python3.9/site-packages/alembic/ddl/impl.py
new file mode 100644
index 00000000..2e4f1ae9
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/ddl/impl.py
@@ -0,0 +1,844 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import logging
+import re
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Mapping
+from typing import NamedTuple
+from typing import Optional
+from typing import Sequence
+from typing import Set
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import cast
+from sqlalchemy import schema
+from sqlalchemy import text
+
+from . import _autogen
+from . import base
+from ._autogen import _constraint_sig as _constraint_sig
+from ._autogen import ComparisonResult as ComparisonResult
+from .. import util
+from ..util import sqla_compat
+
+if TYPE_CHECKING:
+ from typing import Literal
+ from typing import TextIO
+
+ from sqlalchemy.engine import Connection
+ from sqlalchemy.engine import Dialect
+ from sqlalchemy.engine.cursor import CursorResult
+ from sqlalchemy.engine.reflection import Inspector
+ from sqlalchemy.sql import ClauseElement
+ from sqlalchemy.sql import Executable
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.elements import quoted_name
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.schema import ForeignKeyConstraint
+ from sqlalchemy.sql.schema import Index
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.schema import UniqueConstraint
+ from sqlalchemy.sql.selectable import TableClause
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .base import _ServerDefault
+ from ..autogenerate.api import AutogenContext
+ from ..operations.batch import ApplyBatchImpl
+ from ..operations.batch import BatchOperationsImpl
+
+log = logging.getLogger(__name__)
+
+
+class ImplMeta(type):
+ def __init__(
+ cls,
+ classname: str,
+ bases: Tuple[Type[DefaultImpl]],
+ dict_: Dict[str, Any],
+ ):
+ newtype = type.__init__(cls, classname, bases, dict_)
+ if "__dialect__" in dict_:
+ _impls[dict_["__dialect__"]] = cls # type: ignore[assignment]
+ return newtype
+
+
+_impls: Dict[str, Type[DefaultImpl]] = {}
+
+
+class DefaultImpl(metaclass=ImplMeta):
+
+ """Provide the entrypoint for major migration operations,
+ including database-specific behavioral variances.
+
+ While individual SQL/DDL constructs already provide
+ for database-specific implementations, variances here
+ allow for entirely different sequences of operations
+ to take place for a particular migration, such as
+ SQL Server's special 'IDENTITY INSERT' step for
+ bulk inserts.
+
+ """
+
+ __dialect__ = "default"
+
+ transactional_ddl = False
+ command_terminator = ";"
+ type_synonyms: Tuple[Set[str], ...] = ({"NUMERIC", "DECIMAL"},)
+ type_arg_extract: Sequence[str] = ()
+ # These attributes are deprecated in SQLAlchemy via #10247. They need to
+ # be ignored to support older version that did not use dialect kwargs.
+ # They only apply to Oracle and are replaced by oracle_order,
+ # oracle_on_null
+ identity_attrs_ignore: Tuple[str, ...] = ("order", "on_null")
+
+ def __init__(
+ self,
+ dialect: Dialect,
+ connection: Optional[Connection],
+ as_sql: bool,
+ transactional_ddl: Optional[bool],
+ output_buffer: Optional[TextIO],
+ context_opts: Dict[str, Any],
+ ) -> None:
+ self.dialect = dialect
+ self.connection = connection
+ self.as_sql = as_sql
+ self.literal_binds = context_opts.get("literal_binds", False)
+
+ self.output_buffer = output_buffer
+ self.memo: dict = {}
+ self.context_opts = context_opts
+ if transactional_ddl is not None:
+ self.transactional_ddl = transactional_ddl
+
+ if self.literal_binds:
+ if not self.as_sql:
+ raise util.CommandError(
+ "Can't use literal_binds setting without as_sql mode"
+ )
+
+ @classmethod
+ def get_by_dialect(cls, dialect: Dialect) -> Type[DefaultImpl]:
+ return _impls[dialect.name]
+
+ def static_output(self, text: str) -> None:
+ assert self.output_buffer is not None
+ self.output_buffer.write(text + "\n\n")
+ self.output_buffer.flush()
+
+ def requires_recreate_in_batch(
+ self, batch_op: BatchOperationsImpl
+ ) -> bool:
+ """Return True if the given :class:`.BatchOperationsImpl`
+ would need the table to be recreated and copied in order to
+ proceed.
+
+ Normally, only returns True on SQLite when operations other
+ than add_column are present.
+
+ """
+ return False
+
+ def prep_table_for_batch(
+ self, batch_impl: ApplyBatchImpl, table: Table
+ ) -> None:
+ """perform any operations needed on a table before a new
+ one is created to replace it in batch mode.
+
+ the PG dialect uses this to drop constraints on the table
+ before the new one uses those same names.
+
+ """
+
+ @property
+ def bind(self) -> Optional[Connection]:
+ return self.connection
+
+ def _exec(
+ self,
+ construct: Union[Executable, str],
+ execution_options: Optional[dict[str, Any]] = None,
+ multiparams: Sequence[dict] = (),
+ params: Dict[str, Any] = util.immutabledict(),
+ ) -> Optional[CursorResult]:
+ if isinstance(construct, str):
+ construct = text(construct)
+ if self.as_sql:
+ if multiparams or params:
+ # TODO: coverage
+ raise Exception("Execution arguments not allowed with as_sql")
+
+ compile_kw: dict[str, Any]
+ if self.literal_binds and not isinstance(
+ construct, schema.DDLElement
+ ):
+ compile_kw = dict(compile_kwargs={"literal_binds": True})
+ else:
+ compile_kw = {}
+
+ if TYPE_CHECKING:
+ assert isinstance(construct, ClauseElement)
+ compiled = construct.compile(dialect=self.dialect, **compile_kw)
+ self.static_output(
+ str(compiled).replace("\t", " ").strip()
+ + self.command_terminator
+ )
+ return None
+ else:
+ conn = self.connection
+ assert conn is not None
+ if execution_options:
+ conn = conn.execution_options(**execution_options)
+ if params:
+ assert isinstance(multiparams, tuple)
+ multiparams += (params,)
+
+ return conn.execute(construct, multiparams)
+
+ def execute(
+ self,
+ sql: Union[Executable, str],
+ execution_options: Optional[dict[str, Any]] = None,
+ ) -> None:
+ self._exec(sql, execution_options)
+
+ def alter_column(
+ self,
+ table_name: str,
+ column_name: str,
+ nullable: Optional[bool] = None,
+ server_default: Union[_ServerDefault, Literal[False]] = False,
+ name: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ schema: Optional[str] = None,
+ autoincrement: Optional[bool] = None,
+ comment: Optional[Union[str, Literal[False]]] = False,
+ existing_comment: Optional[str] = None,
+ existing_type: Optional[TypeEngine] = None,
+ existing_server_default: Optional[_ServerDefault] = None,
+ existing_nullable: Optional[bool] = None,
+ existing_autoincrement: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ if autoincrement is not None or existing_autoincrement is not None:
+ util.warn(
+ "autoincrement and existing_autoincrement "
+ "only make sense for MySQL",
+ stacklevel=3,
+ )
+ if nullable is not None:
+ self._exec(
+ base.ColumnNullable(
+ table_name,
+ column_name,
+ nullable,
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_comment=existing_comment,
+ )
+ )
+ if server_default is not False:
+ kw = {}
+ cls_: Type[
+ Union[
+ base.ComputedColumnDefault,
+ base.IdentityColumnDefault,
+ base.ColumnDefault,
+ ]
+ ]
+ if sqla_compat._server_default_is_computed(
+ server_default, existing_server_default
+ ):
+ cls_ = base.ComputedColumnDefault
+ elif sqla_compat._server_default_is_identity(
+ server_default, existing_server_default
+ ):
+ cls_ = base.IdentityColumnDefault
+ kw["impl"] = self
+ else:
+ cls_ = base.ColumnDefault
+ self._exec(
+ cls_(
+ table_name,
+ column_name,
+ server_default, # type:ignore[arg-type]
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_comment=existing_comment,
+ **kw,
+ )
+ )
+ if type_ is not None:
+ self._exec(
+ base.ColumnType(
+ table_name,
+ column_name,
+ type_,
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_comment=existing_comment,
+ )
+ )
+
+ if comment is not False:
+ self._exec(
+ base.ColumnComment(
+ table_name,
+ column_name,
+ comment,
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_comment=existing_comment,
+ )
+ )
+
+ # do the new name last ;)
+ if name is not None:
+ self._exec(
+ base.ColumnName(
+ table_name,
+ column_name,
+ name,
+ schema=schema,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ )
+ )
+
+ def add_column(
+ self,
+ table_name: str,
+ column: Column[Any],
+ schema: Optional[Union[str, quoted_name]] = None,
+ ) -> None:
+ self._exec(base.AddColumn(table_name, column, schema=schema))
+
+ def drop_column(
+ self,
+ table_name: str,
+ column: Column[Any],
+ schema: Optional[str] = None,
+ **kw,
+ ) -> None:
+ self._exec(base.DropColumn(table_name, column, schema=schema))
+
+ def add_constraint(self, const: Any) -> None:
+ if const._create_rule is None or const._create_rule(self):
+ self._exec(schema.AddConstraint(const))
+
+ def drop_constraint(self, const: Constraint) -> None:
+ self._exec(schema.DropConstraint(const))
+
+ def rename_table(
+ self,
+ old_table_name: str,
+ new_table_name: Union[str, quoted_name],
+ schema: Optional[Union[str, quoted_name]] = None,
+ ) -> None:
+ self._exec(
+ base.RenameTable(old_table_name, new_table_name, schema=schema)
+ )
+
+ def create_table(self, table: Table) -> None:
+ table.dispatch.before_create(
+ table, self.connection, checkfirst=False, _ddl_runner=self
+ )
+ self._exec(schema.CreateTable(table))
+ table.dispatch.after_create(
+ table, self.connection, checkfirst=False, _ddl_runner=self
+ )
+ for index in table.indexes:
+ self._exec(schema.CreateIndex(index))
+
+ with_comment = (
+ self.dialect.supports_comments and not self.dialect.inline_comments
+ )
+ comment = table.comment
+ if comment and with_comment:
+ self.create_table_comment(table)
+
+ for column in table.columns:
+ comment = column.comment
+ if comment and with_comment:
+ self.create_column_comment(column)
+
+ def drop_table(self, table: Table) -> None:
+ table.dispatch.before_drop(
+ table, self.connection, checkfirst=False, _ddl_runner=self
+ )
+ self._exec(schema.DropTable(table))
+ table.dispatch.after_drop(
+ table, self.connection, checkfirst=False, _ddl_runner=self
+ )
+
+ def create_index(self, index: Index, **kw: Any) -> None:
+ self._exec(schema.CreateIndex(index, **kw))
+
+ def create_table_comment(self, table: Table) -> None:
+ self._exec(schema.SetTableComment(table))
+
+ def drop_table_comment(self, table: Table) -> None:
+ self._exec(schema.DropTableComment(table))
+
+ def create_column_comment(self, column: ColumnElement[Any]) -> None:
+ self._exec(schema.SetColumnComment(column))
+
+ def drop_index(self, index: Index, **kw: Any) -> None:
+ self._exec(schema.DropIndex(index, **kw))
+
+ def bulk_insert(
+ self,
+ table: Union[TableClause, Table],
+ rows: List[dict],
+ multiinsert: bool = True,
+ ) -> None:
+ if not isinstance(rows, list):
+ raise TypeError("List expected")
+ elif rows and not isinstance(rows[0], dict):
+ raise TypeError("List of dictionaries expected")
+ if self.as_sql:
+ for row in rows:
+ self._exec(
+ sqla_compat._insert_inline(table).values(
+ **{
+ k: sqla_compat._literal_bindparam(
+ k, v, type_=table.c[k].type
+ )
+ if not isinstance(
+ v, sqla_compat._literal_bindparam
+ )
+ else v
+ for k, v in row.items()
+ }
+ )
+ )
+ else:
+ if rows:
+ if multiinsert:
+ self._exec(
+ sqla_compat._insert_inline(table), multiparams=rows
+ )
+ else:
+ for row in rows:
+ self._exec(
+ sqla_compat._insert_inline(table).values(**row)
+ )
+
+ def _tokenize_column_type(self, column: Column) -> Params:
+ definition: str
+ definition = self.dialect.type_compiler.process(column.type).lower()
+
+ # tokenize the SQLAlchemy-generated version of a type, so that
+ # the two can be compared.
+ #
+ # examples:
+ # NUMERIC(10, 5)
+ # TIMESTAMP WITH TIMEZONE
+ # INTEGER UNSIGNED
+ # INTEGER (10) UNSIGNED
+ # INTEGER(10) UNSIGNED
+ # varchar character set utf8
+ #
+
+ tokens: List[str] = re.findall(r"[\w\-_]+|\(.+?\)", definition)
+
+ term_tokens: List[str] = []
+ paren_term = None
+
+ for token in tokens:
+ if re.match(r"^\(.*\)$", token):
+ paren_term = token
+ else:
+ term_tokens.append(token)
+
+ params = Params(term_tokens[0], term_tokens[1:], [], {})
+
+ if paren_term:
+ term: str
+ for term in re.findall("[^(),]+", paren_term):
+ if "=" in term:
+ key, val = term.split("=")
+ params.kwargs[key.strip()] = val.strip()
+ else:
+ params.args.append(term.strip())
+
+ return params
+
+ def _column_types_match(
+ self, inspector_params: Params, metadata_params: Params
+ ) -> bool:
+ if inspector_params.token0 == metadata_params.token0:
+ return True
+
+ synonyms = [{t.lower() for t in batch} for batch in self.type_synonyms]
+ inspector_all_terms = " ".join(
+ [inspector_params.token0] + inspector_params.tokens
+ )
+ metadata_all_terms = " ".join(
+ [metadata_params.token0] + metadata_params.tokens
+ )
+
+ for batch in synonyms:
+ if {inspector_all_terms, metadata_all_terms}.issubset(batch) or {
+ inspector_params.token0,
+ metadata_params.token0,
+ }.issubset(batch):
+ return True
+ return False
+
+ def _column_args_match(
+ self, inspected_params: Params, meta_params: Params
+ ) -> bool:
+ """We want to compare column parameters. However, we only want
+ to compare parameters that are set. If they both have `collation`,
+ we want to make sure they are the same. However, if only one
+ specifies it, dont flag it for being less specific
+ """
+
+ if (
+ len(meta_params.tokens) == len(inspected_params.tokens)
+ and meta_params.tokens != inspected_params.tokens
+ ):
+ return False
+
+ if (
+ len(meta_params.args) == len(inspected_params.args)
+ and meta_params.args != inspected_params.args
+ ):
+ return False
+
+ insp = " ".join(inspected_params.tokens).lower()
+ meta = " ".join(meta_params.tokens).lower()
+
+ for reg in self.type_arg_extract:
+ mi = re.search(reg, insp)
+ mm = re.search(reg, meta)
+
+ if mi and mm and mi.group(1) != mm.group(1):
+ return False
+
+ return True
+
+ def compare_type(
+ self, inspector_column: Column[Any], metadata_column: Column
+ ) -> bool:
+ """Returns True if there ARE differences between the types of the two
+ columns. Takes impl.type_synonyms into account between retrospected
+ and metadata types
+ """
+ inspector_params = self._tokenize_column_type(inspector_column)
+ metadata_params = self._tokenize_column_type(metadata_column)
+
+ if not self._column_types_match(inspector_params, metadata_params):
+ return True
+ if not self._column_args_match(inspector_params, metadata_params):
+ return True
+ return False
+
+ def compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_inspector_default,
+ ):
+ return rendered_inspector_default != rendered_metadata_default
+
+ def correct_for_autogen_constraints(
+ self,
+ conn_uniques: Set[UniqueConstraint],
+ conn_indexes: Set[Index],
+ metadata_unique_constraints: Set[UniqueConstraint],
+ metadata_indexes: Set[Index],
+ ) -> None:
+ pass
+
+ def cast_for_batch_migrate(self, existing, existing_transfer, new_type):
+ if existing.type._type_affinity is not new_type._type_affinity:
+ existing_transfer["expr"] = cast(
+ existing_transfer["expr"], new_type
+ )
+
+ def render_ddl_sql_expr(
+ self, expr: ClauseElement, is_server_default: bool = False, **kw: Any
+ ) -> str:
+ """Render a SQL expression that is typically a server default,
+ index expression, etc.
+
+ """
+
+ compile_kw = {"literal_binds": True, "include_table": False}
+
+ return str(
+ expr.compile(dialect=self.dialect, compile_kwargs=compile_kw)
+ )
+
+ def _compat_autogen_column_reflect(self, inspector: Inspector) -> Callable:
+ return self.autogen_column_reflect
+
+ def correct_for_autogen_foreignkeys(
+ self,
+ conn_fks: Set[ForeignKeyConstraint],
+ metadata_fks: Set[ForeignKeyConstraint],
+ ) -> None:
+ pass
+
+ def autogen_column_reflect(self, inspector, table, column_info):
+ """A hook that is attached to the 'column_reflect' event for when
+ a Table is reflected from the database during the autogenerate
+ process.
+
+ Dialects can elect to modify the information gathered here.
+
+ """
+
+ def start_migrations(self) -> None:
+ """A hook called when :meth:`.EnvironmentContext.run_migrations`
+ is called.
+
+ Implementations can set up per-migration-run state here.
+
+ """
+
+ def emit_begin(self) -> None:
+ """Emit the string ``BEGIN``, or the backend-specific
+ equivalent, on the current connection context.
+
+ This is used in offline mode and typically
+ via :meth:`.EnvironmentContext.begin_transaction`.
+
+ """
+ self.static_output("BEGIN" + self.command_terminator)
+
+ def emit_commit(self) -> None:
+ """Emit the string ``COMMIT``, or the backend-specific
+ equivalent, on the current connection context.
+
+ This is used in offline mode and typically
+ via :meth:`.EnvironmentContext.begin_transaction`.
+
+ """
+ self.static_output("COMMIT" + self.command_terminator)
+
+ def render_type(
+ self, type_obj: TypeEngine, autogen_context: AutogenContext
+ ) -> Union[str, Literal[False]]:
+ return False
+
+ def _compare_identity_default(self, metadata_identity, inspector_identity):
+ # ignored contains the attributes that were not considered
+ # because assumed to their default values in the db.
+ diff, ignored = _compare_identity_options(
+ metadata_identity,
+ inspector_identity,
+ sqla_compat.Identity(),
+ skip={"always"},
+ )
+
+ meta_always = getattr(metadata_identity, "always", None)
+ inspector_always = getattr(inspector_identity, "always", None)
+ # None and False are the same in this comparison
+ if bool(meta_always) != bool(inspector_always):
+ diff.add("always")
+
+ diff.difference_update(self.identity_attrs_ignore)
+
+ # returns 3 values:
+ return (
+ # different identity attributes
+ diff,
+ # ignored identity attributes
+ ignored,
+ # if the two identity should be considered different
+ bool(diff) or bool(metadata_identity) != bool(inspector_identity),
+ )
+
+ def _compare_index_unique(
+ self, metadata_index: Index, reflected_index: Index
+ ) -> Optional[str]:
+ conn_unique = bool(reflected_index.unique)
+ meta_unique = bool(metadata_index.unique)
+ if conn_unique != meta_unique:
+ return f"unique={conn_unique} to unique={meta_unique}"
+ else:
+ return None
+
+ def _create_metadata_constraint_sig(
+ self, constraint: _autogen._C, **opts: Any
+ ) -> _constraint_sig[_autogen._C]:
+ return _constraint_sig.from_constraint(True, self, constraint, **opts)
+
+ def _create_reflected_constraint_sig(
+ self, constraint: _autogen._C, **opts: Any
+ ) -> _constraint_sig[_autogen._C]:
+ return _constraint_sig.from_constraint(False, self, constraint, **opts)
+
+ def compare_indexes(
+ self,
+ metadata_index: Index,
+ reflected_index: Index,
+ ) -> ComparisonResult:
+ """Compare two indexes by comparing the signature generated by
+ ``create_index_sig``.
+
+ This method returns a ``ComparisonResult``.
+ """
+ msg: List[str] = []
+ unique_msg = self._compare_index_unique(
+ metadata_index, reflected_index
+ )
+ if unique_msg:
+ msg.append(unique_msg)
+ m_sig = self._create_metadata_constraint_sig(metadata_index)
+ r_sig = self._create_reflected_constraint_sig(reflected_index)
+
+ assert _autogen.is_index_sig(m_sig)
+ assert _autogen.is_index_sig(r_sig)
+
+ # The assumption is that the index have no expression
+ for sig in m_sig, r_sig:
+ if sig.has_expressions:
+ log.warning(
+ "Generating approximate signature for index %s. "
+ "The dialect "
+ "implementation should either skip expression indexes "
+ "or provide a custom implementation.",
+ sig.const,
+ )
+
+ if m_sig.column_names != r_sig.column_names:
+ msg.append(
+ f"expression {r_sig.column_names} to {m_sig.column_names}"
+ )
+
+ if msg:
+ return ComparisonResult.Different(msg)
+ else:
+ return ComparisonResult.Equal()
+
+ def compare_unique_constraint(
+ self,
+ metadata_constraint: UniqueConstraint,
+ reflected_constraint: UniqueConstraint,
+ ) -> ComparisonResult:
+ """Compare two unique constraints by comparing the two signatures.
+
+ The arguments are two tuples that contain the unique constraint and
+ the signatures generated by ``create_unique_constraint_sig``.
+
+ This method returns a ``ComparisonResult``.
+ """
+ metadata_tup = self._create_metadata_constraint_sig(
+ metadata_constraint
+ )
+ reflected_tup = self._create_reflected_constraint_sig(
+ reflected_constraint
+ )
+
+ meta_sig = metadata_tup.unnamed
+ conn_sig = reflected_tup.unnamed
+ if conn_sig != meta_sig:
+ return ComparisonResult.Different(
+ f"expression {conn_sig} to {meta_sig}"
+ )
+ else:
+ return ComparisonResult.Equal()
+
+ def _skip_functional_indexes(self, metadata_indexes, conn_indexes):
+ conn_indexes_by_name = {c.name: c for c in conn_indexes}
+
+ for idx in list(metadata_indexes):
+ if idx.name in conn_indexes_by_name:
+ continue
+ iex = sqla_compat.is_expression_index(idx)
+ if iex:
+ util.warn(
+ "autogenerate skipping metadata-specified "
+ "expression-based index "
+ f"{idx.name!r}; dialect {self.__dialect__!r} under "
+ f"SQLAlchemy {sqla_compat.sqlalchemy_version} can't "
+ "reflect these indexes so they can't be compared"
+ )
+ metadata_indexes.discard(idx)
+
+ def adjust_reflected_dialect_options(
+ self, reflected_object: Dict[str, Any], kind: str
+ ) -> Dict[str, Any]:
+ return reflected_object.get("dialect_options", {})
+
+
+class Params(NamedTuple):
+ token0: str
+ tokens: List[str]
+ args: List[str]
+ kwargs: Dict[str, str]
+
+
+def _compare_identity_options(
+ metadata_io: Union[schema.Identity, schema.Sequence, None],
+ inspector_io: Union[schema.Identity, schema.Sequence, None],
+ default_io: Union[schema.Identity, schema.Sequence],
+ skip: Set[str],
+):
+ # this can be used for identity or sequence compare.
+ # default_io is an instance of IdentityOption with all attributes to the
+ # default value.
+ meta_d = sqla_compat._get_identity_options_dict(metadata_io)
+ insp_d = sqla_compat._get_identity_options_dict(inspector_io)
+
+ diff = set()
+ ignored_attr = set()
+
+ def check_dicts(
+ meta_dict: Mapping[str, Any],
+ insp_dict: Mapping[str, Any],
+ default_dict: Mapping[str, Any],
+ attrs: Iterable[str],
+ ):
+ for attr in set(attrs).difference(skip):
+ meta_value = meta_dict.get(attr)
+ insp_value = insp_dict.get(attr)
+ if insp_value != meta_value:
+ default_value = default_dict.get(attr)
+ if meta_value == default_value:
+ ignored_attr.add(attr)
+ else:
+ diff.add(attr)
+
+ check_dicts(
+ meta_d,
+ insp_d,
+ sqla_compat._get_identity_options_dict(default_io),
+ set(meta_d).union(insp_d),
+ )
+ if sqla_compat.identity_has_dialect_kwargs:
+ # use only the dialect kwargs in inspector_io since metadata_io
+ # can have options for many backends
+ check_dicts(
+ getattr(metadata_io, "dialect_kwargs", {}),
+ getattr(inspector_io, "dialect_kwargs", {}),
+ default_io.dialect_kwargs, # type: ignore[union-attr]
+ getattr(inspector_io, "dialect_kwargs", {}),
+ )
+
+ return diff, ignored_attr
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/ddl/mssql.py b/backend/venv/lib/python3.9/site-packages/alembic/ddl/mssql.py
new file mode 100644
index 00000000..baa43d5e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/ddl/mssql.py
@@ -0,0 +1,419 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import re
+from typing import Any
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import types as sqltypes
+from sqlalchemy.schema import Column
+from sqlalchemy.schema import CreateIndex
+from sqlalchemy.sql.base import Executable
+from sqlalchemy.sql.elements import ClauseElement
+
+from .base import AddColumn
+from .base import alter_column
+from .base import alter_table
+from .base import ColumnDefault
+from .base import ColumnName
+from .base import ColumnNullable
+from .base import ColumnType
+from .base import format_column_name
+from .base import format_server_default
+from .base import format_table_name
+from .base import format_type
+from .base import RenameTable
+from .impl import DefaultImpl
+from .. import util
+from ..util import sqla_compat
+from ..util.sqla_compat import compiles
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy.dialects.mssql.base import MSDDLCompiler
+ from sqlalchemy.dialects.mssql.base import MSSQLCompiler
+ from sqlalchemy.engine.cursor import CursorResult
+ from sqlalchemy.sql.schema import Index
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.selectable import TableClause
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .base import _ServerDefault
+
+
+class MSSQLImpl(DefaultImpl):
+ __dialect__ = "mssql"
+ transactional_ddl = True
+ batch_separator = "GO"
+
+ type_synonyms = DefaultImpl.type_synonyms + ({"VARCHAR", "NVARCHAR"},)
+ identity_attrs_ignore = DefaultImpl.identity_attrs_ignore + (
+ "minvalue",
+ "maxvalue",
+ "nominvalue",
+ "nomaxvalue",
+ "cycle",
+ "cache",
+ )
+
+ def __init__(self, *arg, **kw) -> None:
+ super().__init__(*arg, **kw)
+ self.batch_separator = self.context_opts.get(
+ "mssql_batch_separator", self.batch_separator
+ )
+
+ def _exec(self, construct: Any, *args, **kw) -> Optional[CursorResult]:
+ result = super()._exec(construct, *args, **kw)
+ if self.as_sql and self.batch_separator:
+ self.static_output(self.batch_separator)
+ return result
+
+ def emit_begin(self) -> None:
+ self.static_output("BEGIN TRANSACTION" + self.command_terminator)
+
+ def emit_commit(self) -> None:
+ super().emit_commit()
+ if self.as_sql and self.batch_separator:
+ self.static_output(self.batch_separator)
+
+ def alter_column( # type:ignore[override]
+ self,
+ table_name: str,
+ column_name: str,
+ nullable: Optional[bool] = None,
+ server_default: Optional[
+ Union[_ServerDefault, Literal[False]]
+ ] = False,
+ name: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ schema: Optional[str] = None,
+ existing_type: Optional[TypeEngine] = None,
+ existing_server_default: Optional[_ServerDefault] = None,
+ existing_nullable: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ if nullable is not None:
+ if type_ is not None:
+ # the NULL/NOT NULL alter will handle
+ # the type alteration
+ existing_type = type_
+ type_ = None
+ elif existing_type is None:
+ raise util.CommandError(
+ "MS-SQL ALTER COLUMN operations "
+ "with NULL or NOT NULL require the "
+ "existing_type or a new type_ be passed."
+ )
+ elif existing_nullable is not None and type_ is not None:
+ nullable = existing_nullable
+
+ # the NULL/NOT NULL alter will handle
+ # the type alteration
+ existing_type = type_
+ type_ = None
+
+ elif type_ is not None:
+ util.warn(
+ "MS-SQL ALTER COLUMN operations that specify type_= "
+ "should also specify a nullable= or "
+ "existing_nullable= argument to avoid implicit conversion "
+ "of NOT NULL columns to NULL."
+ )
+
+ used_default = False
+ if sqla_compat._server_default_is_identity(
+ server_default, existing_server_default
+ ) or sqla_compat._server_default_is_computed(
+ server_default, existing_server_default
+ ):
+ used_default = True
+ kw["server_default"] = server_default
+ kw["existing_server_default"] = existing_server_default
+
+ super().alter_column(
+ table_name,
+ column_name,
+ nullable=nullable,
+ type_=type_,
+ schema=schema,
+ existing_type=existing_type,
+ existing_nullable=existing_nullable,
+ **kw,
+ )
+
+ if server_default is not False and used_default is False:
+ if existing_server_default is not False or server_default is None:
+ self._exec(
+ _ExecDropConstraint(
+ table_name,
+ column_name,
+ "sys.default_constraints",
+ schema,
+ )
+ )
+ if server_default is not None:
+ super().alter_column(
+ table_name,
+ column_name,
+ schema=schema,
+ server_default=server_default,
+ )
+
+ if name is not None:
+ super().alter_column(
+ table_name, column_name, schema=schema, name=name
+ )
+
+ def create_index(self, index: Index, **kw: Any) -> None:
+ # this likely defaults to None if not present, so get()
+ # should normally not return the default value. being
+ # defensive in any case
+ mssql_include = index.kwargs.get("mssql_include", None) or ()
+ assert index.table is not None
+ for col in mssql_include:
+ if col not in index.table.c:
+ index.table.append_column(Column(col, sqltypes.NullType))
+ self._exec(CreateIndex(index, **kw))
+
+ def bulk_insert( # type:ignore[override]
+ self, table: Union[TableClause, Table], rows: List[dict], **kw: Any
+ ) -> None:
+ if self.as_sql:
+ self._exec(
+ "SET IDENTITY_INSERT %s ON"
+ % self.dialect.identifier_preparer.format_table(table)
+ )
+ super().bulk_insert(table, rows, **kw)
+ self._exec(
+ "SET IDENTITY_INSERT %s OFF"
+ % self.dialect.identifier_preparer.format_table(table)
+ )
+ else:
+ super().bulk_insert(table, rows, **kw)
+
+ def drop_column(
+ self,
+ table_name: str,
+ column: Column[Any],
+ schema: Optional[str] = None,
+ **kw,
+ ) -> None:
+ drop_default = kw.pop("mssql_drop_default", False)
+ if drop_default:
+ self._exec(
+ _ExecDropConstraint(
+ table_name, column, "sys.default_constraints", schema
+ )
+ )
+ drop_check = kw.pop("mssql_drop_check", False)
+ if drop_check:
+ self._exec(
+ _ExecDropConstraint(
+ table_name, column, "sys.check_constraints", schema
+ )
+ )
+ drop_fks = kw.pop("mssql_drop_foreign_key", False)
+ if drop_fks:
+ self._exec(_ExecDropFKConstraint(table_name, column, schema))
+ super().drop_column(table_name, column, schema=schema, **kw)
+
+ def compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_inspector_default,
+ ):
+ if rendered_metadata_default is not None:
+ rendered_metadata_default = re.sub(
+ r"[\(\) \"\']", "", rendered_metadata_default
+ )
+
+ if rendered_inspector_default is not None:
+ # SQL Server collapses whitespace and adds arbitrary parenthesis
+ # within expressions. our only option is collapse all of it
+
+ rendered_inspector_default = re.sub(
+ r"[\(\) \"\']", "", rendered_inspector_default
+ )
+
+ return rendered_inspector_default != rendered_metadata_default
+
+ def _compare_identity_default(self, metadata_identity, inspector_identity):
+ diff, ignored, is_alter = super()._compare_identity_default(
+ metadata_identity, inspector_identity
+ )
+
+ if (
+ metadata_identity is None
+ and inspector_identity is not None
+ and not diff
+ and inspector_identity.column is not None
+ and inspector_identity.column.primary_key
+ ):
+ # mssql reflect primary keys with autoincrement as identity
+ # columns. if no different attributes are present ignore them
+ is_alter = False
+
+ return diff, ignored, is_alter
+
+ def adjust_reflected_dialect_options(
+ self, reflected_object: Dict[str, Any], kind: str
+ ) -> Dict[str, Any]:
+ options: Dict[str, Any]
+ options = reflected_object.get("dialect_options", {}).copy()
+ if not options.get("mssql_include"):
+ options.pop("mssql_include", None)
+ if not options.get("mssql_clustered"):
+ options.pop("mssql_clustered", None)
+ return options
+
+
+class _ExecDropConstraint(Executable, ClauseElement):
+ inherit_cache = False
+
+ def __init__(
+ self,
+ tname: str,
+ colname: Union[Column[Any], str],
+ type_: str,
+ schema: Optional[str],
+ ) -> None:
+ self.tname = tname
+ self.colname = colname
+ self.type_ = type_
+ self.schema = schema
+
+
+class _ExecDropFKConstraint(Executable, ClauseElement):
+ inherit_cache = False
+
+ def __init__(
+ self, tname: str, colname: Column[Any], schema: Optional[str]
+ ) -> None:
+ self.tname = tname
+ self.colname = colname
+ self.schema = schema
+
+
+@compiles(_ExecDropConstraint, "mssql")
+def _exec_drop_col_constraint(
+ element: _ExecDropConstraint, compiler: MSSQLCompiler, **kw
+) -> str:
+ schema, tname, colname, type_ = (
+ element.schema,
+ element.tname,
+ element.colname,
+ element.type_,
+ )
+ # from http://www.mssqltips.com/sqlservertip/1425/\
+ # working-with-default-constraints-in-sql-server/
+ return """declare @const_name varchar(256)
+select @const_name = QUOTENAME([name]) from %(type)s
+where parent_object_id = object_id('%(schema_dot)s%(tname)s')
+and col_name(parent_object_id, parent_column_id) = '%(colname)s'
+exec('alter table %(tname_quoted)s drop constraint ' + @const_name)""" % {
+ "type": type_,
+ "tname": tname,
+ "colname": colname,
+ "tname_quoted": format_table_name(compiler, tname, schema),
+ "schema_dot": schema + "." if schema else "",
+ }
+
+
+@compiles(_ExecDropFKConstraint, "mssql")
+def _exec_drop_col_fk_constraint(
+ element: _ExecDropFKConstraint, compiler: MSSQLCompiler, **kw
+) -> str:
+ schema, tname, colname = element.schema, element.tname, element.colname
+
+ return """declare @const_name varchar(256)
+select @const_name = QUOTENAME([name]) from
+sys.foreign_keys fk join sys.foreign_key_columns fkc
+on fk.object_id=fkc.constraint_object_id
+where fkc.parent_object_id = object_id('%(schema_dot)s%(tname)s')
+and col_name(fkc.parent_object_id, fkc.parent_column_id) = '%(colname)s'
+exec('alter table %(tname_quoted)s drop constraint ' + @const_name)""" % {
+ "tname": tname,
+ "colname": colname,
+ "tname_quoted": format_table_name(compiler, tname, schema),
+ "schema_dot": schema + "." if schema else "",
+ }
+
+
+@compiles(AddColumn, "mssql")
+def visit_add_column(element: AddColumn, compiler: MSDDLCompiler, **kw) -> str:
+ return "%s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ mssql_add_column(compiler, element.column, **kw),
+ )
+
+
+def mssql_add_column(
+ compiler: MSDDLCompiler, column: Column[Any], **kw
+) -> str:
+ return "ADD %s" % compiler.get_column_specification(column, **kw)
+
+
+@compiles(ColumnNullable, "mssql")
+def visit_column_nullable(
+ element: ColumnNullable, compiler: MSDDLCompiler, **kw
+) -> str:
+ return "%s %s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ format_type(compiler, element.existing_type), # type: ignore[arg-type]
+ "NULL" if element.nullable else "NOT NULL",
+ )
+
+
+@compiles(ColumnDefault, "mssql")
+def visit_column_default(
+ element: ColumnDefault, compiler: MSDDLCompiler, **kw
+) -> str:
+ # TODO: there can also be a named constraint
+ # with ADD CONSTRAINT here
+ return "%s ADD DEFAULT %s FOR %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_server_default(compiler, element.default),
+ format_column_name(compiler, element.column_name),
+ )
+
+
+@compiles(ColumnName, "mssql")
+def visit_rename_column(
+ element: ColumnName, compiler: MSDDLCompiler, **kw
+) -> str:
+ return "EXEC sp_rename '%s.%s', %s, 'COLUMN'" % (
+ format_table_name(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ format_column_name(compiler, element.newname),
+ )
+
+
+@compiles(ColumnType, "mssql")
+def visit_column_type(
+ element: ColumnType, compiler: MSDDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ format_type(compiler, element.type_),
+ )
+
+
+@compiles(RenameTable, "mssql")
+def visit_rename_table(
+ element: RenameTable, compiler: MSDDLCompiler, **kw
+) -> str:
+ return "EXEC sp_rename '%s', %s" % (
+ format_table_name(compiler, element.table_name, element.schema),
+ format_table_name(compiler, element.new_table_name, None),
+ )
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/ddl/mysql.py b/backend/venv/lib/python3.9/site-packages/alembic/ddl/mysql.py
new file mode 100644
index 00000000..f312173e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/ddl/mysql.py
@@ -0,0 +1,474 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import re
+from typing import Any
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import schema
+from sqlalchemy import types as sqltypes
+
+from .base import alter_table
+from .base import AlterColumn
+from .base import ColumnDefault
+from .base import ColumnName
+from .base import ColumnNullable
+from .base import ColumnType
+from .base import format_column_name
+from .base import format_server_default
+from .impl import DefaultImpl
+from .. import util
+from ..util import sqla_compat
+from ..util.sqla_compat import _is_mariadb
+from ..util.sqla_compat import _is_type_bound
+from ..util.sqla_compat import compiles
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy.dialects.mysql.base import MySQLDDLCompiler
+ from sqlalchemy.sql.ddl import DropConstraint
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .base import _ServerDefault
+
+
+class MySQLImpl(DefaultImpl):
+ __dialect__ = "mysql"
+
+ transactional_ddl = False
+ type_synonyms = DefaultImpl.type_synonyms + (
+ {"BOOL", "TINYINT"},
+ {"JSON", "LONGTEXT"},
+ )
+ type_arg_extract = [r"character set ([\w\-_]+)", r"collate ([\w\-_]+)"]
+
+ def alter_column( # type:ignore[override]
+ self,
+ table_name: str,
+ column_name: str,
+ nullable: Optional[bool] = None,
+ server_default: Union[_ServerDefault, Literal[False]] = False,
+ name: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ schema: Optional[str] = None,
+ existing_type: Optional[TypeEngine] = None,
+ existing_server_default: Optional[_ServerDefault] = None,
+ existing_nullable: Optional[bool] = None,
+ autoincrement: Optional[bool] = None,
+ existing_autoincrement: Optional[bool] = None,
+ comment: Optional[Union[str, Literal[False]]] = False,
+ existing_comment: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ if sqla_compat._server_default_is_identity(
+ server_default, existing_server_default
+ ) or sqla_compat._server_default_is_computed(
+ server_default, existing_server_default
+ ):
+ # modifying computed or identity columns is not supported
+ # the default will raise
+ super().alter_column(
+ table_name,
+ column_name,
+ nullable=nullable,
+ type_=type_,
+ schema=schema,
+ existing_type=existing_type,
+ existing_nullable=existing_nullable,
+ server_default=server_default,
+ existing_server_default=existing_server_default,
+ **kw,
+ )
+ if name is not None or self._is_mysql_allowed_functional_default(
+ type_ if type_ is not None else existing_type, server_default
+ ):
+ self._exec(
+ MySQLChangeColumn(
+ table_name,
+ column_name,
+ schema=schema,
+ newname=name if name is not None else column_name,
+ nullable=nullable
+ if nullable is not None
+ else existing_nullable
+ if existing_nullable is not None
+ else True,
+ type_=type_ if type_ is not None else existing_type,
+ default=server_default
+ if server_default is not False
+ else existing_server_default,
+ autoincrement=autoincrement
+ if autoincrement is not None
+ else existing_autoincrement,
+ comment=comment
+ if comment is not False
+ else existing_comment,
+ )
+ )
+ elif (
+ nullable is not None
+ or type_ is not None
+ or autoincrement is not None
+ or comment is not False
+ ):
+ self._exec(
+ MySQLModifyColumn(
+ table_name,
+ column_name,
+ schema=schema,
+ newname=name if name is not None else column_name,
+ nullable=nullable
+ if nullable is not None
+ else existing_nullable
+ if existing_nullable is not None
+ else True,
+ type_=type_ if type_ is not None else existing_type,
+ default=server_default
+ if server_default is not False
+ else existing_server_default,
+ autoincrement=autoincrement
+ if autoincrement is not None
+ else existing_autoincrement,
+ comment=comment
+ if comment is not False
+ else existing_comment,
+ )
+ )
+ elif server_default is not False:
+ self._exec(
+ MySQLAlterDefault(
+ table_name, column_name, server_default, schema=schema
+ )
+ )
+
+ def drop_constraint(
+ self,
+ const: Constraint,
+ ) -> None:
+ if isinstance(const, schema.CheckConstraint) and _is_type_bound(const):
+ return
+
+ super().drop_constraint(const)
+
+ def _is_mysql_allowed_functional_default(
+ self,
+ type_: Optional[TypeEngine],
+ server_default: Union[_ServerDefault, Literal[False]],
+ ) -> bool:
+ return (
+ type_ is not None
+ and type_._type_affinity is sqltypes.DateTime
+ and server_default is not None
+ )
+
+ def compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_inspector_default,
+ ):
+ # partially a workaround for SQLAlchemy issue #3023; if the
+ # column were created without "NOT NULL", MySQL may have added
+ # an implicit default of '0' which we need to skip
+ # TODO: this is not really covered anymore ?
+ if (
+ metadata_column.type._type_affinity is sqltypes.Integer
+ and inspector_column.primary_key
+ and not inspector_column.autoincrement
+ and not rendered_metadata_default
+ and rendered_inspector_default == "'0'"
+ ):
+ return False
+ elif (
+ rendered_inspector_default
+ and inspector_column.type._type_affinity is sqltypes.Integer
+ ):
+ rendered_inspector_default = (
+ re.sub(r"^'|'$", "", rendered_inspector_default)
+ if rendered_inspector_default is not None
+ else None
+ )
+ return rendered_inspector_default != rendered_metadata_default
+ elif (
+ rendered_metadata_default
+ and metadata_column.type._type_affinity is sqltypes.String
+ ):
+ metadata_default = re.sub(r"^'|'$", "", rendered_metadata_default)
+ return rendered_inspector_default != f"'{metadata_default}'"
+ elif rendered_inspector_default and rendered_metadata_default:
+ # adjust for "function()" vs. "FUNCTION" as can occur particularly
+ # for the CURRENT_TIMESTAMP function on newer MariaDB versions
+
+ # SQLAlchemy MySQL dialect bundles ON UPDATE into the server
+ # default; adjust for this possibly being present.
+ onupdate_ins = re.match(
+ r"(.*) (on update.*?)(?:\(\))?$",
+ rendered_inspector_default.lower(),
+ )
+ onupdate_met = re.match(
+ r"(.*) (on update.*?)(?:\(\))?$",
+ rendered_metadata_default.lower(),
+ )
+
+ if onupdate_ins:
+ if not onupdate_met:
+ return True
+ elif onupdate_ins.group(2) != onupdate_met.group(2):
+ return True
+
+ rendered_inspector_default = onupdate_ins.group(1)
+ rendered_metadata_default = onupdate_met.group(1)
+
+ return re.sub(
+ r"(.*?)(?:\(\))?$", r"\1", rendered_inspector_default.lower()
+ ) != re.sub(
+ r"(.*?)(?:\(\))?$", r"\1", rendered_metadata_default.lower()
+ )
+ else:
+ return rendered_inspector_default != rendered_metadata_default
+
+ def correct_for_autogen_constraints(
+ self,
+ conn_unique_constraints,
+ conn_indexes,
+ metadata_unique_constraints,
+ metadata_indexes,
+ ):
+ # TODO: if SQLA 1.0, make use of "duplicates_index"
+ # metadata
+ removed = set()
+ for idx in list(conn_indexes):
+ if idx.unique:
+ continue
+ # MySQL puts implicit indexes on FK columns, even if
+ # composite and even if MyISAM, so can't check this too easily.
+ # the name of the index may be the column name or it may
+ # be the name of the FK constraint.
+ for col in idx.columns:
+ if idx.name == col.name:
+ conn_indexes.remove(idx)
+ removed.add(idx.name)
+ break
+ for fk in col.foreign_keys:
+ if fk.name == idx.name:
+ conn_indexes.remove(idx)
+ removed.add(idx.name)
+ break
+ if idx.name in removed:
+ break
+
+ # then remove indexes from the "metadata_indexes"
+ # that we've removed from reflected, otherwise they come out
+ # as adds (see #202)
+ for idx in list(metadata_indexes):
+ if idx.name in removed:
+ metadata_indexes.remove(idx)
+
+ def correct_for_autogen_foreignkeys(self, conn_fks, metadata_fks):
+ conn_fk_by_sig = {
+ self._create_reflected_constraint_sig(fk).unnamed_no_options: fk
+ for fk in conn_fks
+ }
+ metadata_fk_by_sig = {
+ self._create_metadata_constraint_sig(fk).unnamed_no_options: fk
+ for fk in metadata_fks
+ }
+
+ for sig in set(conn_fk_by_sig).intersection(metadata_fk_by_sig):
+ mdfk = metadata_fk_by_sig[sig]
+ cnfk = conn_fk_by_sig[sig]
+ # MySQL considers RESTRICT to be the default and doesn't
+ # report on it. if the model has explicit RESTRICT and
+ # the conn FK has None, set it to RESTRICT
+ if (
+ mdfk.ondelete is not None
+ and mdfk.ondelete.lower() == "restrict"
+ and cnfk.ondelete is None
+ ):
+ cnfk.ondelete = "RESTRICT"
+ if (
+ mdfk.onupdate is not None
+ and mdfk.onupdate.lower() == "restrict"
+ and cnfk.onupdate is None
+ ):
+ cnfk.onupdate = "RESTRICT"
+
+
+class MariaDBImpl(MySQLImpl):
+ __dialect__ = "mariadb"
+
+
+class MySQLAlterDefault(AlterColumn):
+ def __init__(
+ self,
+ name: str,
+ column_name: str,
+ default: _ServerDefault,
+ schema: Optional[str] = None,
+ ) -> None:
+ super(AlterColumn, self).__init__(name, schema=schema)
+ self.column_name = column_name
+ self.default = default
+
+
+class MySQLChangeColumn(AlterColumn):
+ def __init__(
+ self,
+ name: str,
+ column_name: str,
+ schema: Optional[str] = None,
+ newname: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ nullable: Optional[bool] = None,
+ default: Optional[Union[_ServerDefault, Literal[False]]] = False,
+ autoincrement: Optional[bool] = None,
+ comment: Optional[Union[str, Literal[False]]] = False,
+ ) -> None:
+ super(AlterColumn, self).__init__(name, schema=schema)
+ self.column_name = column_name
+ self.nullable = nullable
+ self.newname = newname
+ self.default = default
+ self.autoincrement = autoincrement
+ self.comment = comment
+ if type_ is None:
+ raise util.CommandError(
+ "All MySQL CHANGE/MODIFY COLUMN operations "
+ "require the existing type."
+ )
+
+ self.type_ = sqltypes.to_instance(type_)
+
+
+class MySQLModifyColumn(MySQLChangeColumn):
+ pass
+
+
+@compiles(ColumnNullable, "mysql", "mariadb")
+@compiles(ColumnName, "mysql", "mariadb")
+@compiles(ColumnDefault, "mysql", "mariadb")
+@compiles(ColumnType, "mysql", "mariadb")
+def _mysql_doesnt_support_individual(element, compiler, **kw):
+ raise NotImplementedError(
+ "Individual alter column constructs not supported by MySQL"
+ )
+
+
+@compiles(MySQLAlterDefault, "mysql", "mariadb")
+def _mysql_alter_default(
+ element: MySQLAlterDefault, compiler: MySQLDDLCompiler, **kw
+) -> str:
+ return "%s ALTER COLUMN %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ "SET DEFAULT %s" % format_server_default(compiler, element.default)
+ if element.default is not None
+ else "DROP DEFAULT",
+ )
+
+
+@compiles(MySQLModifyColumn, "mysql", "mariadb")
+def _mysql_modify_column(
+ element: MySQLModifyColumn, compiler: MySQLDDLCompiler, **kw
+) -> str:
+ return "%s MODIFY %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ _mysql_colspec(
+ compiler,
+ nullable=element.nullable,
+ server_default=element.default,
+ type_=element.type_,
+ autoincrement=element.autoincrement,
+ comment=element.comment,
+ ),
+ )
+
+
+@compiles(MySQLChangeColumn, "mysql", "mariadb")
+def _mysql_change_column(
+ element: MySQLChangeColumn, compiler: MySQLDDLCompiler, **kw
+) -> str:
+ return "%s CHANGE %s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ format_column_name(compiler, element.newname),
+ _mysql_colspec(
+ compiler,
+ nullable=element.nullable,
+ server_default=element.default,
+ type_=element.type_,
+ autoincrement=element.autoincrement,
+ comment=element.comment,
+ ),
+ )
+
+
+def _mysql_colspec(
+ compiler: MySQLDDLCompiler,
+ nullable: Optional[bool],
+ server_default: Optional[Union[_ServerDefault, Literal[False]]],
+ type_: TypeEngine,
+ autoincrement: Optional[bool],
+ comment: Optional[Union[str, Literal[False]]],
+) -> str:
+ spec = "%s %s" % (
+ compiler.dialect.type_compiler.process(type_),
+ "NULL" if nullable else "NOT NULL",
+ )
+ if autoincrement:
+ spec += " AUTO_INCREMENT"
+ if server_default is not False and server_default is not None:
+ spec += " DEFAULT %s" % format_server_default(compiler, server_default)
+ if comment:
+ spec += " COMMENT %s" % compiler.sql_compiler.render_literal_value(
+ comment, sqltypes.String()
+ )
+
+ return spec
+
+
+@compiles(schema.DropConstraint, "mysql", "mariadb")
+def _mysql_drop_constraint(
+ element: DropConstraint, compiler: MySQLDDLCompiler, **kw
+) -> str:
+ """Redefine SQLAlchemy's drop constraint to
+ raise errors for invalid constraint type."""
+
+ constraint = element.element
+ if isinstance(
+ constraint,
+ (
+ schema.ForeignKeyConstraint,
+ schema.PrimaryKeyConstraint,
+ schema.UniqueConstraint,
+ ),
+ ):
+ assert not kw
+ return compiler.visit_drop_constraint(element)
+ elif isinstance(constraint, schema.CheckConstraint):
+ # note that SQLAlchemy as of 1.2 does not yet support
+ # DROP CONSTRAINT for MySQL/MariaDB, so we implement fully
+ # here.
+ if _is_mariadb(compiler.dialect):
+ return "ALTER TABLE %s DROP CONSTRAINT %s" % (
+ compiler.preparer.format_table(constraint.table),
+ compiler.preparer.format_constraint(constraint),
+ )
+ else:
+ return "ALTER TABLE %s DROP CHECK %s" % (
+ compiler.preparer.format_table(constraint.table),
+ compiler.preparer.format_constraint(constraint),
+ )
+ else:
+ raise NotImplementedError(
+ "No generic 'DROP CONSTRAINT' in MySQL - "
+ "please specify constraint type"
+ )
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/ddl/oracle.py b/backend/venv/lib/python3.9/site-packages/alembic/ddl/oracle.py
new file mode 100644
index 00000000..54011740
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/ddl/oracle.py
@@ -0,0 +1,200 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import re
+from typing import Any
+from typing import Optional
+from typing import TYPE_CHECKING
+
+from sqlalchemy.sql import sqltypes
+
+from .base import AddColumn
+from .base import alter_table
+from .base import ColumnComment
+from .base import ColumnDefault
+from .base import ColumnName
+from .base import ColumnNullable
+from .base import ColumnType
+from .base import format_column_name
+from .base import format_server_default
+from .base import format_table_name
+from .base import format_type
+from .base import IdentityColumnDefault
+from .base import RenameTable
+from .impl import DefaultImpl
+from ..util.sqla_compat import compiles
+
+if TYPE_CHECKING:
+ from sqlalchemy.dialects.oracle.base import OracleDDLCompiler
+ from sqlalchemy.engine.cursor import CursorResult
+ from sqlalchemy.sql.schema import Column
+
+
+class OracleImpl(DefaultImpl):
+ __dialect__ = "oracle"
+ transactional_ddl = False
+ batch_separator = "/"
+ command_terminator = ""
+ type_synonyms = DefaultImpl.type_synonyms + (
+ {"VARCHAR", "VARCHAR2"},
+ {"BIGINT", "INTEGER", "SMALLINT", "DECIMAL", "NUMERIC", "NUMBER"},
+ {"DOUBLE", "FLOAT", "DOUBLE_PRECISION"},
+ )
+ identity_attrs_ignore = ()
+
+ def __init__(self, *arg, **kw) -> None:
+ super().__init__(*arg, **kw)
+ self.batch_separator = self.context_opts.get(
+ "oracle_batch_separator", self.batch_separator
+ )
+
+ def _exec(self, construct: Any, *args, **kw) -> Optional[CursorResult]:
+ result = super()._exec(construct, *args, **kw)
+ if self.as_sql and self.batch_separator:
+ self.static_output(self.batch_separator)
+ return result
+
+ def compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_inspector_default,
+ ):
+ if rendered_metadata_default is not None:
+ rendered_metadata_default = re.sub(
+ r"^\((.+)\)$", r"\1", rendered_metadata_default
+ )
+
+ rendered_metadata_default = re.sub(
+ r"^\"?'(.+)'\"?$", r"\1", rendered_metadata_default
+ )
+
+ if rendered_inspector_default is not None:
+ rendered_inspector_default = re.sub(
+ r"^\((.+)\)$", r"\1", rendered_inspector_default
+ )
+
+ rendered_inspector_default = re.sub(
+ r"^\"?'(.+)'\"?$", r"\1", rendered_inspector_default
+ )
+
+ rendered_inspector_default = rendered_inspector_default.strip()
+ return rendered_inspector_default != rendered_metadata_default
+
+ def emit_begin(self) -> None:
+ self._exec("SET TRANSACTION READ WRITE")
+
+ def emit_commit(self) -> None:
+ self._exec("COMMIT")
+
+
+@compiles(AddColumn, "oracle")
+def visit_add_column(
+ element: AddColumn, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ add_column(compiler, element.column, **kw),
+ )
+
+
+@compiles(ColumnNullable, "oracle")
+def visit_column_nullable(
+ element: ColumnNullable, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "NULL" if element.nullable else "NOT NULL",
+ )
+
+
+@compiles(ColumnType, "oracle")
+def visit_column_type(
+ element: ColumnType, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "%s" % format_type(compiler, element.type_),
+ )
+
+
+@compiles(ColumnName, "oracle")
+def visit_column_name(
+ element: ColumnName, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s RENAME COLUMN %s TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_column_name(compiler, element.column_name),
+ format_column_name(compiler, element.newname),
+ )
+
+
+@compiles(ColumnDefault, "oracle")
+def visit_column_default(
+ element: ColumnDefault, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "DEFAULT %s" % format_server_default(compiler, element.default)
+ if element.default is not None
+ else "DEFAULT NULL",
+ )
+
+
+@compiles(ColumnComment, "oracle")
+def visit_column_comment(
+ element: ColumnComment, compiler: OracleDDLCompiler, **kw
+) -> str:
+ ddl = "COMMENT ON COLUMN {table_name}.{column_name} IS {comment}"
+
+ comment = compiler.sql_compiler.render_literal_value(
+ (element.comment if element.comment is not None else ""),
+ sqltypes.String(),
+ )
+
+ return ddl.format(
+ table_name=element.table_name,
+ column_name=element.column_name,
+ comment=comment,
+ )
+
+
+@compiles(RenameTable, "oracle")
+def visit_rename_table(
+ element: RenameTable, compiler: OracleDDLCompiler, **kw
+) -> str:
+ return "%s RENAME TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_table_name(compiler, element.new_table_name, None),
+ )
+
+
+def alter_column(compiler: OracleDDLCompiler, name: str) -> str:
+ return "MODIFY %s" % format_column_name(compiler, name)
+
+
+def add_column(compiler: OracleDDLCompiler, column: Column[Any], **kw) -> str:
+ return "ADD %s" % compiler.get_column_specification(column, **kw)
+
+
+@compiles(IdentityColumnDefault, "oracle")
+def visit_identity_column(
+ element: IdentityColumnDefault, compiler: OracleDDLCompiler, **kw
+):
+ text = "%s %s " % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ )
+ if element.default is None:
+ # drop identity
+ text += "DROP IDENTITY"
+ return text
+ else:
+ text += compiler.visit_identity_column(element.default)
+ return text
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/ddl/postgresql.py b/backend/venv/lib/python3.9/site-packages/alembic/ddl/postgresql.py
new file mode 100644
index 00000000..6507fcbd
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/ddl/postgresql.py
@@ -0,0 +1,848 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import logging
+import re
+from typing import Any
+from typing import cast
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import Sequence
+from typing import Tuple
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import Column
+from sqlalchemy import literal_column
+from sqlalchemy import Numeric
+from sqlalchemy import text
+from sqlalchemy import types as sqltypes
+from sqlalchemy.dialects.postgresql import BIGINT
+from sqlalchemy.dialects.postgresql import ExcludeConstraint
+from sqlalchemy.dialects.postgresql import INTEGER
+from sqlalchemy.schema import CreateIndex
+from sqlalchemy.sql.elements import ColumnClause
+from sqlalchemy.sql.elements import TextClause
+from sqlalchemy.sql.functions import FunctionElement
+from sqlalchemy.types import NULLTYPE
+
+from .base import alter_column
+from .base import alter_table
+from .base import AlterColumn
+from .base import ColumnComment
+from .base import format_column_name
+from .base import format_table_name
+from .base import format_type
+from .base import IdentityColumnDefault
+from .base import RenameTable
+from .impl import ComparisonResult
+from .impl import DefaultImpl
+from .. import util
+from ..autogenerate import render
+from ..operations import ops
+from ..operations import schemaobj
+from ..operations.base import BatchOperations
+from ..operations.base import Operations
+from ..util import sqla_compat
+from ..util.sqla_compat import compiles
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy import Index
+ from sqlalchemy import UniqueConstraint
+ from sqlalchemy.dialects.postgresql.array import ARRAY
+ from sqlalchemy.dialects.postgresql.base import PGDDLCompiler
+ from sqlalchemy.dialects.postgresql.hstore import HSTORE
+ from sqlalchemy.dialects.postgresql.json import JSON
+ from sqlalchemy.dialects.postgresql.json import JSONB
+ from sqlalchemy.sql.elements import ClauseElement
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.elements import quoted_name
+ from sqlalchemy.sql.schema import MetaData
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from .base import _ServerDefault
+ from ..autogenerate.api import AutogenContext
+ from ..autogenerate.render import _f_name
+ from ..runtime.migration import MigrationContext
+
+
+log = logging.getLogger(__name__)
+
+
+class PostgresqlImpl(DefaultImpl):
+ __dialect__ = "postgresql"
+ transactional_ddl = True
+ type_synonyms = DefaultImpl.type_synonyms + (
+ {"FLOAT", "DOUBLE PRECISION"},
+ )
+
+ def create_index(self, index: Index, **kw: Any) -> None:
+ # this likely defaults to None if not present, so get()
+ # should normally not return the default value. being
+ # defensive in any case
+ postgresql_include = index.kwargs.get("postgresql_include", None) or ()
+ for col in postgresql_include:
+ if col not in index.table.c: # type: ignore[union-attr]
+ index.table.append_column( # type: ignore[union-attr]
+ Column(col, sqltypes.NullType)
+ )
+ self._exec(CreateIndex(index, **kw))
+
+ def prep_table_for_batch(self, batch_impl, table):
+ for constraint in table.constraints:
+ if (
+ constraint.name is not None
+ and constraint.name in batch_impl.named_constraints
+ ):
+ self.drop_constraint(constraint)
+
+ def compare_server_default(
+ self,
+ inspector_column,
+ metadata_column,
+ rendered_metadata_default,
+ rendered_inspector_default,
+ ):
+ # don't do defaults for SERIAL columns
+ if (
+ metadata_column.primary_key
+ and metadata_column is metadata_column.table._autoincrement_column
+ ):
+ return False
+
+ conn_col_default = rendered_inspector_default
+
+ defaults_equal = conn_col_default == rendered_metadata_default
+ if defaults_equal:
+ return False
+
+ if None in (
+ conn_col_default,
+ rendered_metadata_default,
+ metadata_column.server_default,
+ ):
+ return not defaults_equal
+
+ metadata_default = metadata_column.server_default.arg
+
+ if isinstance(metadata_default, str):
+ if not isinstance(inspector_column.type, Numeric):
+ metadata_default = re.sub(r"^'|'$", "", metadata_default)
+ metadata_default = f"'{metadata_default}'"
+
+ metadata_default = literal_column(metadata_default)
+
+ # run a real compare against the server
+ conn = self.connection
+ assert conn is not None
+ return not conn.scalar(
+ sqla_compat._select(
+ literal_column(conn_col_default) == metadata_default
+ )
+ )
+
+ def alter_column( # type:ignore[override]
+ self,
+ table_name: str,
+ column_name: str,
+ nullable: Optional[bool] = None,
+ server_default: Union[_ServerDefault, Literal[False]] = False,
+ name: Optional[str] = None,
+ type_: Optional[TypeEngine] = None,
+ schema: Optional[str] = None,
+ autoincrement: Optional[bool] = None,
+ existing_type: Optional[TypeEngine] = None,
+ existing_server_default: Optional[_ServerDefault] = None,
+ existing_nullable: Optional[bool] = None,
+ existing_autoincrement: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ using = kw.pop("postgresql_using", None)
+
+ if using is not None and type_ is None:
+ raise util.CommandError(
+ "postgresql_using must be used with the type_ parameter"
+ )
+
+ if type_ is not None:
+ self._exec(
+ PostgresqlColumnType(
+ table_name,
+ column_name,
+ type_,
+ schema=schema,
+ using=using,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ )
+ )
+
+ super().alter_column(
+ table_name,
+ column_name,
+ nullable=nullable,
+ server_default=server_default,
+ name=name,
+ schema=schema,
+ autoincrement=autoincrement,
+ existing_type=existing_type,
+ existing_server_default=existing_server_default,
+ existing_nullable=existing_nullable,
+ existing_autoincrement=existing_autoincrement,
+ **kw,
+ )
+
+ def autogen_column_reflect(self, inspector, table, column_info):
+ if column_info.get("default") and isinstance(
+ column_info["type"], (INTEGER, BIGINT)
+ ):
+ seq_match = re.match(
+ r"nextval\('(.+?)'::regclass\)", column_info["default"]
+ )
+ if seq_match:
+ info = sqla_compat._exec_on_inspector(
+ inspector,
+ text(
+ "select c.relname, a.attname "
+ "from pg_class as c join "
+ "pg_depend d on d.objid=c.oid and "
+ "d.classid='pg_class'::regclass and "
+ "d.refclassid='pg_class'::regclass "
+ "join pg_class t on t.oid=d.refobjid "
+ "join pg_attribute a on a.attrelid=t.oid and "
+ "a.attnum=d.refobjsubid "
+ "where c.relkind='S' and c.relname=:seqname"
+ ),
+ seqname=seq_match.group(1),
+ ).first()
+ if info:
+ seqname, colname = info
+ if colname == column_info["name"]:
+ log.info(
+ "Detected sequence named '%s' as "
+ "owned by integer column '%s(%s)', "
+ "assuming SERIAL and omitting",
+ seqname,
+ table.name,
+ colname,
+ )
+ # sequence, and the owner is this column,
+ # its a SERIAL - whack it!
+ del column_info["default"]
+
+ def correct_for_autogen_constraints(
+ self,
+ conn_unique_constraints,
+ conn_indexes,
+ metadata_unique_constraints,
+ metadata_indexes,
+ ):
+ doubled_constraints = {
+ index
+ for index in conn_indexes
+ if index.info.get("duplicates_constraint")
+ }
+
+ for ix in doubled_constraints:
+ conn_indexes.remove(ix)
+
+ if not sqla_compat.sqla_2:
+ self._skip_functional_indexes(metadata_indexes, conn_indexes)
+
+ # pg behavior regarding modifiers
+ # | # | compiled sql | returned sql | regexp. group is removed |
+ # | - | ---------------- | -----------------| ------------------------ |
+ # | 1 | nulls first | nulls first | - |
+ # | 2 | nulls last | | (? str:
+ expr = expr.lower().replace('"', "").replace("'", "")
+ if index.table is not None:
+ # should not be needed, since include_table=False is in compile
+ expr = expr.replace(f"{index.table.name.lower()}.", "")
+
+ if "::" in expr:
+ # strip :: cast. types can have spaces in them
+ expr = re.sub(r"(::[\w ]+\w)", "", expr)
+
+ while expr and expr[0] == "(" and expr[-1] == ")":
+ expr = expr[1:-1]
+
+ # NOTE: when parsing the connection expression this cleanup could
+ # be skipped
+ for rs in self._default_modifiers_re:
+ if match := rs.search(expr):
+ start, end = match.span(1)
+ expr = expr[:start] + expr[end:]
+ break
+
+ while expr and expr[0] == "(" and expr[-1] == ")":
+ expr = expr[1:-1]
+
+ # strip casts
+ cast_re = re.compile(r"cast\s*\(")
+ if cast_re.match(expr):
+ expr = cast_re.sub("", expr)
+ # remove the as type
+ expr = re.sub(r"as\s+[^)]+\)", "", expr)
+ # remove spaces
+ expr = expr.replace(" ", "")
+ return expr
+
+ def _dialect_options(
+ self, item: Union[Index, UniqueConstraint]
+ ) -> Tuple[Any, ...]:
+ # only the positive case is returned by sqlalchemy reflection so
+ # None and False are threated the same
+ if item.dialect_kwargs.get("postgresql_nulls_not_distinct"):
+ return ("nulls_not_distinct",)
+ return ()
+
+ def compare_indexes(
+ self,
+ metadata_index: Index,
+ reflected_index: Index,
+ ) -> ComparisonResult:
+ msg = []
+ unique_msg = self._compare_index_unique(
+ metadata_index, reflected_index
+ )
+ if unique_msg:
+ msg.append(unique_msg)
+ m_exprs = metadata_index.expressions
+ r_exprs = reflected_index.expressions
+ if len(m_exprs) != len(r_exprs):
+ msg.append(f"expression number {len(r_exprs)} to {len(m_exprs)}")
+ if msg:
+ # no point going further, return early
+ return ComparisonResult.Different(msg)
+ skip = []
+ for pos, (m_e, r_e) in enumerate(zip(m_exprs, r_exprs), 1):
+ m_compile = self._compile_element(m_e)
+ m_text = self._cleanup_index_expr(metadata_index, m_compile)
+ # print(f"META ORIG: {m_compile!r} CLEANUP: {m_text!r}")
+ r_compile = self._compile_element(r_e)
+ r_text = self._cleanup_index_expr(metadata_index, r_compile)
+ # print(f"CONN ORIG: {r_compile!r} CLEANUP: {r_text!r}")
+ if m_text == r_text:
+ continue # expressions these are equal
+ elif m_compile.strip().endswith("_ops") and (
+ " " in m_compile or ")" in m_compile # is an expression
+ ):
+ skip.append(
+ f"expression #{pos} {m_compile!r} detected "
+ "as including operator clause."
+ )
+ util.warn(
+ f"Expression #{pos} {m_compile!r} in index "
+ f"{reflected_index.name!r} detected to include "
+ "an operator clause. Expression compare cannot proceed. "
+ "Please move the operator clause to the "
+ "``postgresql_ops`` dict to enable proper compare "
+ "of the index expressions: "
+ "https://docs.sqlalchemy.org/en/latest/dialects/postgresql.html#operator-classes", # noqa: E501
+ )
+ else:
+ msg.append(f"expression #{pos} {r_compile!r} to {m_compile!r}")
+
+ m_options = self._dialect_options(metadata_index)
+ r_options = self._dialect_options(reflected_index)
+ if m_options != r_options:
+ msg.extend(f"options {r_options} to {m_options}")
+
+ if msg:
+ return ComparisonResult.Different(msg)
+ elif skip:
+ # if there are other changes detected don't skip the index
+ return ComparisonResult.Skip(skip)
+ else:
+ return ComparisonResult.Equal()
+
+ def compare_unique_constraint(
+ self,
+ metadata_constraint: UniqueConstraint,
+ reflected_constraint: UniqueConstraint,
+ ) -> ComparisonResult:
+ metadata_tup = self._create_metadata_constraint_sig(
+ metadata_constraint
+ )
+ reflected_tup = self._create_reflected_constraint_sig(
+ reflected_constraint
+ )
+
+ meta_sig = metadata_tup.unnamed
+ conn_sig = reflected_tup.unnamed
+ if conn_sig != meta_sig:
+ return ComparisonResult.Different(
+ f"expression {conn_sig} to {meta_sig}"
+ )
+
+ metadata_do = self._dialect_options(metadata_tup.const)
+ conn_do = self._dialect_options(reflected_tup.const)
+ if metadata_do != conn_do:
+ return ComparisonResult.Different(
+ f"expression {conn_do} to {metadata_do}"
+ )
+
+ return ComparisonResult.Equal()
+
+ def adjust_reflected_dialect_options(
+ self, reflected_options: Dict[str, Any], kind: str
+ ) -> Dict[str, Any]:
+ options: Dict[str, Any]
+ options = reflected_options.get("dialect_options", {}).copy()
+ if not options.get("postgresql_include"):
+ options.pop("postgresql_include", None)
+ return options
+
+ def _compile_element(self, element: Union[ClauseElement, str]) -> str:
+ if isinstance(element, str):
+ return element
+ return element.compile(
+ dialect=self.dialect,
+ compile_kwargs={"literal_binds": True, "include_table": False},
+ ).string
+
+ def render_ddl_sql_expr(
+ self,
+ expr: ClauseElement,
+ is_server_default: bool = False,
+ is_index: bool = False,
+ **kw: Any,
+ ) -> str:
+ """Render a SQL expression that is typically a server default,
+ index expression, etc.
+
+ """
+
+ # apply self_group to index expressions;
+ # see https://github.com/sqlalchemy/sqlalchemy/blob/
+ # 82fa95cfce070fab401d020c6e6e4a6a96cc2578/
+ # lib/sqlalchemy/dialects/postgresql/base.py#L2261
+ if is_index and not isinstance(expr, ColumnClause):
+ expr = expr.self_group()
+
+ return super().render_ddl_sql_expr(
+ expr, is_server_default=is_server_default, is_index=is_index, **kw
+ )
+
+ def render_type(
+ self, type_: TypeEngine, autogen_context: AutogenContext
+ ) -> Union[str, Literal[False]]:
+ mod = type(type_).__module__
+ if not mod.startswith("sqlalchemy.dialects.postgresql"):
+ return False
+
+ if hasattr(self, "_render_%s_type" % type_.__visit_name__):
+ meth = getattr(self, "_render_%s_type" % type_.__visit_name__)
+ return meth(type_, autogen_context)
+
+ return False
+
+ def _render_HSTORE_type(
+ self, type_: HSTORE, autogen_context: AutogenContext
+ ) -> str:
+ return cast(
+ str,
+ render._render_type_w_subtype(
+ type_, autogen_context, "text_type", r"(.+?\(.*text_type=)"
+ ),
+ )
+
+ def _render_ARRAY_type(
+ self, type_: ARRAY, autogen_context: AutogenContext
+ ) -> str:
+ return cast(
+ str,
+ render._render_type_w_subtype(
+ type_, autogen_context, "item_type", r"(.+?\()"
+ ),
+ )
+
+ def _render_JSON_type(
+ self, type_: JSON, autogen_context: AutogenContext
+ ) -> str:
+ return cast(
+ str,
+ render._render_type_w_subtype(
+ type_, autogen_context, "astext_type", r"(.+?\(.*astext_type=)"
+ ),
+ )
+
+ def _render_JSONB_type(
+ self, type_: JSONB, autogen_context: AutogenContext
+ ) -> str:
+ return cast(
+ str,
+ render._render_type_w_subtype(
+ type_, autogen_context, "astext_type", r"(.+?\(.*astext_type=)"
+ ),
+ )
+
+
+class PostgresqlColumnType(AlterColumn):
+ def __init__(
+ self, name: str, column_name: str, type_: TypeEngine, **kw
+ ) -> None:
+ using = kw.pop("using", None)
+ super().__init__(name, column_name, **kw)
+ self.type_ = sqltypes.to_instance(type_)
+ self.using = using
+
+
+@compiles(RenameTable, "postgresql")
+def visit_rename_table(
+ element: RenameTable, compiler: PGDDLCompiler, **kw
+) -> str:
+ return "%s RENAME TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_table_name(compiler, element.new_table_name, None),
+ )
+
+
+@compiles(PostgresqlColumnType, "postgresql")
+def visit_column_type(
+ element: PostgresqlColumnType, compiler: PGDDLCompiler, **kw
+) -> str:
+ return "%s %s %s %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ "TYPE %s" % format_type(compiler, element.type_),
+ "USING %s" % element.using if element.using else "",
+ )
+
+
+@compiles(ColumnComment, "postgresql")
+def visit_column_comment(
+ element: ColumnComment, compiler: PGDDLCompiler, **kw
+) -> str:
+ ddl = "COMMENT ON COLUMN {table_name}.{column_name} IS {comment}"
+ comment = (
+ compiler.sql_compiler.render_literal_value(
+ element.comment, sqltypes.String()
+ )
+ if element.comment is not None
+ else "NULL"
+ )
+
+ return ddl.format(
+ table_name=format_table_name(
+ compiler, element.table_name, element.schema
+ ),
+ column_name=format_column_name(compiler, element.column_name),
+ comment=comment,
+ )
+
+
+@compiles(IdentityColumnDefault, "postgresql")
+def visit_identity_column(
+ element: IdentityColumnDefault, compiler: PGDDLCompiler, **kw
+):
+ text = "%s %s " % (
+ alter_table(compiler, element.table_name, element.schema),
+ alter_column(compiler, element.column_name),
+ )
+ if element.default is None:
+ # drop identity
+ text += "DROP IDENTITY"
+ return text
+ elif element.existing_server_default is None:
+ # add identity options
+ text += "ADD "
+ text += compiler.visit_identity_column(element.default)
+ return text
+ else:
+ # alter identity
+ diff, _, _ = element.impl._compare_identity_default(
+ element.default, element.existing_server_default
+ )
+ identity = element.default
+ for attr in sorted(diff):
+ if attr == "always":
+ text += "SET GENERATED %s " % (
+ "ALWAYS" if identity.always else "BY DEFAULT"
+ )
+ else:
+ text += "SET %s " % compiler.get_identity_options(
+ sqla_compat.Identity(**{attr: getattr(identity, attr)})
+ )
+ return text
+
+
+@Operations.register_operation("create_exclude_constraint")
+@BatchOperations.register_operation(
+ "create_exclude_constraint", "batch_create_exclude_constraint"
+)
+@ops.AddConstraintOp.register_add_constraint("exclude_constraint")
+class CreateExcludeConstraintOp(ops.AddConstraintOp):
+ """Represent a create exclude constraint operation."""
+
+ constraint_type = "exclude"
+
+ def __init__(
+ self,
+ constraint_name: sqla_compat._ConstraintName,
+ table_name: Union[str, quoted_name],
+ elements: Union[
+ Sequence[Tuple[str, str]],
+ Sequence[Tuple[ColumnClause[Any], str]],
+ ],
+ where: Optional[Union[ColumnElement[bool], str]] = None,
+ schema: Optional[str] = None,
+ _orig_constraint: Optional[ExcludeConstraint] = None,
+ **kw,
+ ) -> None:
+ self.constraint_name = constraint_name
+ self.table_name = table_name
+ self.elements = elements
+ self.where = where
+ self.schema = schema
+ self._orig_constraint = _orig_constraint
+ self.kw = kw
+
+ @classmethod
+ def from_constraint( # type:ignore[override]
+ cls, constraint: ExcludeConstraint
+ ) -> CreateExcludeConstraintOp:
+ constraint_table = sqla_compat._table_for_constraint(constraint)
+ return cls(
+ constraint.name,
+ constraint_table.name,
+ [ # type: ignore
+ (expr, op) for expr, name, op in constraint._render_exprs
+ ],
+ where=cast("ColumnElement[bool] | None", constraint.where),
+ schema=constraint_table.schema,
+ _orig_constraint=constraint,
+ deferrable=constraint.deferrable,
+ initially=constraint.initially,
+ using=constraint.using,
+ )
+
+ def to_constraint(
+ self, migration_context: Optional[MigrationContext] = None
+ ) -> ExcludeConstraint:
+ if self._orig_constraint is not None:
+ return self._orig_constraint
+ schema_obj = schemaobj.SchemaObjects(migration_context)
+ t = schema_obj.table(self.table_name, schema=self.schema)
+ excl = ExcludeConstraint(
+ *self.elements,
+ name=self.constraint_name,
+ where=self.where,
+ **self.kw,
+ )
+ for (
+ expr,
+ name,
+ oper,
+ ) in excl._render_exprs:
+ t.append_column(Column(name, NULLTYPE))
+ t.append_constraint(excl)
+ return excl
+
+ @classmethod
+ def create_exclude_constraint(
+ cls,
+ operations: Operations,
+ constraint_name: str,
+ table_name: str,
+ *elements: Any,
+ **kw: Any,
+ ) -> Optional[Table]:
+ """Issue an alter to create an EXCLUDE constraint using the
+ current migration context.
+
+ .. note:: This method is Postgresql specific, and additionally
+ requires at least SQLAlchemy 1.0.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_exclude_constraint(
+ "user_excl",
+ "user",
+ ("period", "&&"),
+ ("group", "="),
+ where=("group != 'some group'"),
+ )
+
+ Note that the expressions work the same way as that of
+ the ``ExcludeConstraint`` object itself; if plain strings are
+ passed, quoting rules must be applied manually.
+
+ :param name: Name of the constraint.
+ :param table_name: String name of the source table.
+ :param elements: exclude conditions.
+ :param where: SQL expression or SQL string with optional WHERE
+ clause.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within.
+
+ """
+ op = cls(constraint_name, table_name, elements, **kw)
+ return operations.invoke(op)
+
+ @classmethod
+ def batch_create_exclude_constraint(
+ cls,
+ operations: BatchOperations,
+ constraint_name: str,
+ *elements: Any,
+ **kw: Any,
+ ) -> Optional[Table]:
+ """Issue a "create exclude constraint" instruction using the
+ current batch migration context.
+
+ .. note:: This method is Postgresql specific, and additionally
+ requires at least SQLAlchemy 1.0.
+
+ .. seealso::
+
+ :meth:`.Operations.create_exclude_constraint`
+
+ """
+ kw["schema"] = operations.impl.schema
+ op = cls(constraint_name, operations.impl.table_name, elements, **kw)
+ return operations.invoke(op)
+
+
+@render.renderers.dispatch_for(CreateExcludeConstraintOp)
+def _add_exclude_constraint(
+ autogen_context: AutogenContext, op: CreateExcludeConstraintOp
+) -> str:
+ return _exclude_constraint(op.to_constraint(), autogen_context, alter=True)
+
+
+@render._constraint_renderers.dispatch_for(ExcludeConstraint)
+def _render_inline_exclude_constraint(
+ constraint: ExcludeConstraint,
+ autogen_context: AutogenContext,
+ namespace_metadata: MetaData,
+) -> str:
+ rendered = render._user_defined_render(
+ "exclude", constraint, autogen_context
+ )
+ if rendered is not False:
+ return rendered
+
+ return _exclude_constraint(constraint, autogen_context, False)
+
+
+def _postgresql_autogenerate_prefix(autogen_context: AutogenContext) -> str:
+ imports = autogen_context.imports
+ if imports is not None:
+ imports.add("from sqlalchemy.dialects import postgresql")
+ return "postgresql."
+
+
+def _exclude_constraint(
+ constraint: ExcludeConstraint,
+ autogen_context: AutogenContext,
+ alter: bool,
+) -> str:
+ opts: List[Tuple[str, Union[quoted_name, str, _f_name, None]]] = []
+
+ has_batch = autogen_context._has_batch
+
+ if constraint.deferrable:
+ opts.append(("deferrable", str(constraint.deferrable)))
+ if constraint.initially:
+ opts.append(("initially", str(constraint.initially)))
+ if constraint.using:
+ opts.append(("using", str(constraint.using)))
+ if not has_batch and alter and constraint.table.schema:
+ opts.append(("schema", render._ident(constraint.table.schema)))
+ if not alter and constraint.name:
+ opts.append(
+ ("name", render._render_gen_name(autogen_context, constraint.name))
+ )
+
+ def do_expr_where_opts():
+ args = [
+ "(%s, %r)"
+ % (
+ _render_potential_column(
+ sqltext, # type:ignore[arg-type]
+ autogen_context,
+ ),
+ opstring,
+ )
+ for sqltext, name, opstring in constraint._render_exprs
+ ]
+ if constraint.where is not None:
+ args.append(
+ "where=%s"
+ % render._render_potential_expr(
+ constraint.where, autogen_context
+ )
+ )
+ args.extend(["%s=%r" % (k, v) for k, v in opts])
+ return args
+
+ if alter:
+ args = [
+ repr(render._render_gen_name(autogen_context, constraint.name))
+ ]
+ if not has_batch:
+ args += [repr(render._ident(constraint.table.name))]
+ args.extend(do_expr_where_opts())
+ return "%(prefix)screate_exclude_constraint(%(args)s)" % {
+ "prefix": render._alembic_autogenerate_prefix(autogen_context),
+ "args": ", ".join(args),
+ }
+ else:
+ args = do_expr_where_opts()
+ return "%(prefix)sExcludeConstraint(%(args)s)" % {
+ "prefix": _postgresql_autogenerate_prefix(autogen_context),
+ "args": ", ".join(args),
+ }
+
+
+def _render_potential_column(
+ value: Union[
+ ColumnClause[Any], Column[Any], TextClause, FunctionElement[Any]
+ ],
+ autogen_context: AutogenContext,
+) -> str:
+ if isinstance(value, ColumnClause):
+ if value.is_literal:
+ # like literal_column("int8range(from, to)") in ExcludeConstraint
+ template = "%(prefix)sliteral_column(%(name)r)"
+ else:
+ template = "%(prefix)scolumn(%(name)r)"
+
+ return template % {
+ "prefix": render._sqlalchemy_autogenerate_prefix(autogen_context),
+ "name": value.name,
+ }
+ else:
+ return render._render_potential_expr(
+ value,
+ autogen_context,
+ wrap_in_text=isinstance(value, (TextClause, FunctionElement)),
+ )
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/ddl/sqlite.py b/backend/venv/lib/python3.9/site-packages/alembic/ddl/sqlite.py
new file mode 100644
index 00000000..762e8ca1
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/ddl/sqlite.py
@@ -0,0 +1,225 @@
+# mypy: allow-untyped-defs, allow-incomplete-defs, allow-untyped-calls
+# mypy: no-warn-return-any, allow-any-generics
+
+from __future__ import annotations
+
+import re
+from typing import Any
+from typing import Dict
+from typing import Optional
+from typing import TYPE_CHECKING
+from typing import Union
+
+from sqlalchemy import cast
+from sqlalchemy import JSON
+from sqlalchemy import schema
+from sqlalchemy import sql
+
+from .base import alter_table
+from .base import format_table_name
+from .base import RenameTable
+from .impl import DefaultImpl
+from .. import util
+from ..util.sqla_compat import compiles
+
+if TYPE_CHECKING:
+ from sqlalchemy.engine.reflection import Inspector
+ from sqlalchemy.sql.compiler import DDLCompiler
+ from sqlalchemy.sql.elements import Cast
+ from sqlalchemy.sql.elements import ClauseElement
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Constraint
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.type_api import TypeEngine
+
+ from ..operations.batch import BatchOperationsImpl
+
+
+class SQLiteImpl(DefaultImpl):
+ __dialect__ = "sqlite"
+
+ transactional_ddl = False
+ """SQLite supports transactional DDL, but pysqlite does not:
+ see: http://bugs.python.org/issue10740
+ """
+
+ def requires_recreate_in_batch(
+ self, batch_op: BatchOperationsImpl
+ ) -> bool:
+ """Return True if the given :class:`.BatchOperationsImpl`
+ would need the table to be recreated and copied in order to
+ proceed.
+
+ Normally, only returns True on SQLite when operations other
+ than add_column are present.
+
+ """
+ for op in batch_op.batch:
+ if op[0] == "add_column":
+ col = op[1][1]
+ if isinstance(
+ col.server_default, schema.DefaultClause
+ ) and isinstance(col.server_default.arg, sql.ClauseElement):
+ return True
+ elif (
+ isinstance(col.server_default, util.sqla_compat.Computed)
+ and col.server_default.persisted
+ ):
+ return True
+ elif op[0] not in ("create_index", "drop_index"):
+ return True
+ else:
+ return False
+
+ def add_constraint(self, const: Constraint):
+ # attempt to distinguish between an
+ # auto-gen constraint and an explicit one
+ if const._create_rule is None:
+ raise NotImplementedError(
+ "No support for ALTER of constraints in SQLite dialect. "
+ "Please refer to the batch mode feature which allows for "
+ "SQLite migrations using a copy-and-move strategy."
+ )
+ elif const._create_rule(self):
+ util.warn(
+ "Skipping unsupported ALTER for "
+ "creation of implicit constraint. "
+ "Please refer to the batch mode feature which allows for "
+ "SQLite migrations using a copy-and-move strategy."
+ )
+
+ def drop_constraint(self, const: Constraint):
+ if const._create_rule is None:
+ raise NotImplementedError(
+ "No support for ALTER of constraints in SQLite dialect. "
+ "Please refer to the batch mode feature which allows for "
+ "SQLite migrations using a copy-and-move strategy."
+ )
+
+ def compare_server_default(
+ self,
+ inspector_column: Column[Any],
+ metadata_column: Column[Any],
+ rendered_metadata_default: Optional[str],
+ rendered_inspector_default: Optional[str],
+ ) -> bool:
+ if rendered_metadata_default is not None:
+ rendered_metadata_default = re.sub(
+ r"^\((.+)\)$", r"\1", rendered_metadata_default
+ )
+
+ rendered_metadata_default = re.sub(
+ r"^\"?'(.+)'\"?$", r"\1", rendered_metadata_default
+ )
+
+ if rendered_inspector_default is not None:
+ rendered_inspector_default = re.sub(
+ r"^\((.+)\)$", r"\1", rendered_inspector_default
+ )
+
+ rendered_inspector_default = re.sub(
+ r"^\"?'(.+)'\"?$", r"\1", rendered_inspector_default
+ )
+
+ return rendered_inspector_default != rendered_metadata_default
+
+ def _guess_if_default_is_unparenthesized_sql_expr(
+ self, expr: Optional[str]
+ ) -> bool:
+ """Determine if a server default is a SQL expression or a constant.
+
+ There are too many assertions that expect server defaults to round-trip
+ identically without parenthesis added so we will add parens only in
+ very specific cases.
+
+ """
+ if not expr:
+ return False
+ elif re.match(r"^[0-9\.]$", expr):
+ return False
+ elif re.match(r"^'.+'$", expr):
+ return False
+ elif re.match(r"^\(.+\)$", expr):
+ return False
+ else:
+ return True
+
+ def autogen_column_reflect(
+ self,
+ inspector: Inspector,
+ table: Table,
+ column_info: Dict[str, Any],
+ ) -> None:
+ # SQLite expression defaults require parenthesis when sent
+ # as DDL
+ if self._guess_if_default_is_unparenthesized_sql_expr(
+ column_info.get("default", None)
+ ):
+ column_info["default"] = "(%s)" % (column_info["default"],)
+
+ def render_ddl_sql_expr(
+ self, expr: ClauseElement, is_server_default: bool = False, **kw
+ ) -> str:
+ # SQLite expression defaults require parenthesis when sent
+ # as DDL
+ str_expr = super().render_ddl_sql_expr(
+ expr, is_server_default=is_server_default, **kw
+ )
+
+ if (
+ is_server_default
+ and self._guess_if_default_is_unparenthesized_sql_expr(str_expr)
+ ):
+ str_expr = "(%s)" % (str_expr,)
+ return str_expr
+
+ def cast_for_batch_migrate(
+ self,
+ existing: Column[Any],
+ existing_transfer: Dict[str, Union[TypeEngine, Cast]],
+ new_type: TypeEngine,
+ ) -> None:
+ if (
+ existing.type._type_affinity is not new_type._type_affinity
+ and not isinstance(new_type, JSON)
+ ):
+ existing_transfer["expr"] = cast(
+ existing_transfer["expr"], new_type
+ )
+
+ def correct_for_autogen_constraints(
+ self,
+ conn_unique_constraints,
+ conn_indexes,
+ metadata_unique_constraints,
+ metadata_indexes,
+ ):
+ self._skip_functional_indexes(metadata_indexes, conn_indexes)
+
+
+@compiles(RenameTable, "sqlite")
+def visit_rename_table(
+ element: RenameTable, compiler: DDLCompiler, **kw
+) -> str:
+ return "%s RENAME TO %s" % (
+ alter_table(compiler, element.table_name, element.schema),
+ format_table_name(compiler, element.new_table_name, None),
+ )
+
+
+# @compiles(AddColumn, 'sqlite')
+# def visit_add_column(element, compiler, **kw):
+# return "%s %s" % (
+# alter_table(compiler, element.table_name, element.schema),
+# add_column(compiler, element.column, **kw)
+# )
+
+
+# def add_column(compiler, column, **kw):
+# text = "ADD COLUMN %s" % compiler.get_column_specification(column, **kw)
+# need to modify SQLAlchemy so that the CHECK associated with a Boolean
+# or Enum gets placed as part of the column constraints, not the Table
+# see ticket 98
+# for const in column.constraints:
+# text += compiler.process(AddConstraint(const))
+# return text
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/environment.py b/backend/venv/lib/python3.9/site-packages/alembic/environment.py
new file mode 100644
index 00000000..adfc93eb
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/environment.py
@@ -0,0 +1 @@
+from .runtime.environment import * # noqa
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/migration.py b/backend/venv/lib/python3.9/site-packages/alembic/migration.py
new file mode 100644
index 00000000..02626e2c
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/migration.py
@@ -0,0 +1 @@
+from .runtime.migration import * # noqa
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/op.py b/backend/venv/lib/python3.9/site-packages/alembic/op.py
new file mode 100644
index 00000000..f3f5fac0
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/op.py
@@ -0,0 +1,5 @@
+from .operations.base import Operations
+
+# create proxy functions for
+# each method on the Operations class.
+Operations.create_module_class_proxy(globals(), locals())
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/op.pyi b/backend/venv/lib/python3.9/site-packages/alembic/op.pyi
new file mode 100644
index 00000000..83deac1e
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/op.pyi
@@ -0,0 +1,1321 @@
+# ### this file stubs are generated by tools/write_pyi.py - do not edit ###
+# ### imports are manually managed
+from __future__ import annotations
+
+from contextlib import contextmanager
+from typing import Any
+from typing import Awaitable
+from typing import Callable
+from typing import Dict
+from typing import Iterator
+from typing import List
+from typing import Literal
+from typing import Mapping
+from typing import Optional
+from typing import overload
+from typing import Sequence
+from typing import Tuple
+from typing import Type
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+if TYPE_CHECKING:
+ from sqlalchemy.engine import Connection
+ from sqlalchemy.sql import Executable
+ from sqlalchemy.sql.elements import ColumnElement
+ from sqlalchemy.sql.elements import conv
+ from sqlalchemy.sql.elements import TextClause
+ from sqlalchemy.sql.expression import TableClause
+ from sqlalchemy.sql.functions import Function
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Computed
+ from sqlalchemy.sql.schema import Identity
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.sql.schema import Table
+ from sqlalchemy.sql.type_api import TypeEngine
+ from sqlalchemy.util import immutabledict
+
+ from .operations.base import BatchOperations
+ from .operations.ops import AddColumnOp
+ from .operations.ops import AddConstraintOp
+ from .operations.ops import AlterColumnOp
+ from .operations.ops import AlterTableOp
+ from .operations.ops import BulkInsertOp
+ from .operations.ops import CreateIndexOp
+ from .operations.ops import CreateTableCommentOp
+ from .operations.ops import CreateTableOp
+ from .operations.ops import DropColumnOp
+ from .operations.ops import DropConstraintOp
+ from .operations.ops import DropIndexOp
+ from .operations.ops import DropTableCommentOp
+ from .operations.ops import DropTableOp
+ from .operations.ops import ExecuteSQLOp
+ from .operations.ops import MigrateOperation
+ from .runtime.migration import MigrationContext
+ from .util.sqla_compat import _literal_bindparam
+
+_T = TypeVar("_T")
+_C = TypeVar("_C", bound=Callable[..., Any])
+
+### end imports ###
+
+def add_column(
+ table_name: str, column: Column[Any], *, schema: Optional[str] = None
+) -> None:
+ """Issue an "add column" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+ from sqlalchemy import Column, String
+
+ op.add_column("organization", Column("name", String()))
+
+ The :meth:`.Operations.add_column` method typically corresponds
+ to the SQL command "ALTER TABLE... ADD COLUMN". Within the scope
+ of this command, the column's name, datatype, nullability,
+ and optional server-generated defaults may be indicated.
+
+ .. note::
+
+ With the exception of NOT NULL constraints or single-column FOREIGN
+ KEY constraints, other kinds of constraints such as PRIMARY KEY,
+ UNIQUE or CHECK constraints **cannot** be generated using this
+ method; for these constraints, refer to operations such as
+ :meth:`.Operations.create_primary_key` and
+ :meth:`.Operations.create_check_constraint`. In particular, the
+ following :class:`~sqlalchemy.schema.Column` parameters are
+ **ignored**:
+
+ * :paramref:`~sqlalchemy.schema.Column.primary_key` - SQL databases
+ typically do not support an ALTER operation that can add
+ individual columns one at a time to an existing primary key
+ constraint, therefore it's less ambiguous to use the
+ :meth:`.Operations.create_primary_key` method, which assumes no
+ existing primary key constraint is present.
+ * :paramref:`~sqlalchemy.schema.Column.unique` - use the
+ :meth:`.Operations.create_unique_constraint` method
+ * :paramref:`~sqlalchemy.schema.Column.index` - use the
+ :meth:`.Operations.create_index` method
+
+
+ The provided :class:`~sqlalchemy.schema.Column` object may include a
+ :class:`~sqlalchemy.schema.ForeignKey` constraint directive,
+ referencing a remote table name. For this specific type of constraint,
+ Alembic will automatically emit a second ALTER statement in order to
+ add the single-column FOREIGN KEY constraint separately::
+
+ from alembic import op
+ from sqlalchemy import Column, INTEGER, ForeignKey
+
+ op.add_column(
+ "organization",
+ Column("account_id", INTEGER, ForeignKey("accounts.id")),
+ )
+
+ The column argument passed to :meth:`.Operations.add_column` is a
+ :class:`~sqlalchemy.schema.Column` construct, used in the same way it's
+ used in SQLAlchemy. In particular, values or functions to be indicated
+ as producing the column's default value on the database side are
+ specified using the ``server_default`` parameter, and not ``default``
+ which only specifies Python-side defaults::
+
+ from alembic import op
+ from sqlalchemy import Column, TIMESTAMP, func
+
+ # specify "DEFAULT NOW" along with the column add
+ op.add_column(
+ "account",
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ :param table_name: String name of the parent table.
+ :param column: a :class:`sqlalchemy.schema.Column` object
+ representing the new column.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def alter_column(
+ table_name: str,
+ column_name: str,
+ *,
+ nullable: Optional[bool] = None,
+ comment: Union[str, Literal[False], None] = False,
+ server_default: Any = False,
+ new_column_name: Optional[str] = None,
+ type_: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None,
+ existing_type: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None,
+ existing_server_default: Union[
+ str, bool, Identity, Computed, None
+ ] = False,
+ existing_nullable: Optional[bool] = None,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+ **kw: Any,
+) -> None:
+ r"""Issue an "alter column" instruction using the
+ current migration context.
+
+ Generally, only that aspect of the column which
+ is being changed, i.e. name, type, nullability,
+ default, needs to be specified. Multiple changes
+ can also be specified at once and the backend should
+ "do the right thing", emitting each change either
+ separately or together as the backend allows.
+
+ MySQL has special requirements here, since MySQL
+ cannot ALTER a column without a full specification.
+ When producing MySQL-compatible migration files,
+ it is recommended that the ``existing_type``,
+ ``existing_server_default``, and ``existing_nullable``
+ parameters be present, if not being altered.
+
+ Type changes which are against the SQLAlchemy
+ "schema" types :class:`~sqlalchemy.types.Boolean`
+ and :class:`~sqlalchemy.types.Enum` may also
+ add or drop constraints which accompany those
+ types on backends that don't support them natively.
+ The ``existing_type`` argument is
+ used in this case to identify and remove a previous
+ constraint that was bound to the type object.
+
+ :param table_name: string name of the target table.
+ :param column_name: string name of the target column,
+ as it exists before the operation begins.
+ :param nullable: Optional; specify ``True`` or ``False``
+ to alter the column's nullability.
+ :param server_default: Optional; specify a string
+ SQL expression, :func:`~sqlalchemy.sql.expression.text`,
+ or :class:`~sqlalchemy.schema.DefaultClause` to indicate
+ an alteration to the column's default value.
+ Set to ``None`` to have the default removed.
+ :param comment: optional string text of a new comment to add to the
+ column.
+ :param new_column_name: Optional; specify a string name here to
+ indicate the new name within a column rename operation.
+ :param type\_: Optional; a :class:`~sqlalchemy.types.TypeEngine`
+ type object to specify a change to the column's type.
+ For SQLAlchemy types that also indicate a constraint (i.e.
+ :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`),
+ the constraint is also generated.
+ :param autoincrement: set the ``AUTO_INCREMENT`` flag of the column;
+ currently understood by the MySQL dialect.
+ :param existing_type: Optional; a
+ :class:`~sqlalchemy.types.TypeEngine`
+ type object to specify the previous type. This
+ is required for all MySQL column alter operations that
+ don't otherwise specify a new type, as well as for
+ when nullability is being changed on a SQL Server
+ column. It is also used if the type is a so-called
+ SQLAlchemy "schema" type which may define a constraint (i.e.
+ :class:`~sqlalchemy.types.Boolean`,
+ :class:`~sqlalchemy.types.Enum`),
+ so that the constraint can be dropped.
+ :param existing_server_default: Optional; The existing
+ default value of the column. Required on MySQL if
+ an existing default is not being changed; else MySQL
+ removes the default.
+ :param existing_nullable: Optional; the existing nullability
+ of the column. Required on MySQL if the existing nullability
+ is not being changed; else MySQL sets this to NULL.
+ :param existing_autoincrement: Optional; the existing autoincrement
+ of the column. Used for MySQL's system of altering a column
+ that specifies ``AUTO_INCREMENT``.
+ :param existing_comment: string text of the existing comment on the
+ column to be maintained. Required on MySQL if the existing comment
+ on the column is not being changed.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param postgresql_using: String argument which will indicate a
+ SQL expression to render within the Postgresql-specific USING clause
+ within ALTER COLUMN. This string is taken directly as raw SQL which
+ must explicitly include any necessary quoting or escaping of tokens
+ within the expression.
+
+ """
+
+@contextmanager
+def batch_alter_table(
+ table_name: str,
+ schema: Optional[str] = None,
+ recreate: Literal["auto", "always", "never"] = "auto",
+ partial_reordering: Optional[Tuple[Any, ...]] = None,
+ copy_from: Optional[Table] = None,
+ table_args: Tuple[Any, ...] = (),
+ table_kwargs: Mapping[str, Any] = immutabledict({}),
+ reflect_args: Tuple[Any, ...] = (),
+ reflect_kwargs: Mapping[str, Any] = immutabledict({}),
+ naming_convention: Optional[Dict[str, str]] = None,
+) -> Iterator[BatchOperations]:
+ """Invoke a series of per-table migrations in batch.
+
+ Batch mode allows a series of operations specific to a table
+ to be syntactically grouped together, and allows for alternate
+ modes of table migration, in particular the "recreate" style of
+ migration required by SQLite.
+
+ "recreate" style is as follows:
+
+ 1. A new table is created with the new specification, based on the
+ migration directives within the batch, using a temporary name.
+
+ 2. the data copied from the existing table to the new table.
+
+ 3. the existing table is dropped.
+
+ 4. the new table is renamed to the existing table name.
+
+ The directive by default will only use "recreate" style on the
+ SQLite backend, and only if directives are present which require
+ this form, e.g. anything other than ``add_column()``. The batch
+ operation on other backends will proceed using standard ALTER TABLE
+ operations.
+
+ The method is used as a context manager, which returns an instance
+ of :class:`.BatchOperations`; this object is the same as
+ :class:`.Operations` except that table names and schema names
+ are omitted. E.g.::
+
+ with op.batch_alter_table("some_table") as batch_op:
+ batch_op.add_column(Column("foo", Integer))
+ batch_op.drop_column("bar")
+
+ The operations within the context manager are invoked at once
+ when the context is ended. When run against SQLite, if the
+ migrations include operations not supported by SQLite's ALTER TABLE,
+ the entire table will be copied to a new one with the new
+ specification, moving all data across as well.
+
+ The copy operation by default uses reflection to retrieve the current
+ structure of the table, and therefore :meth:`.batch_alter_table`
+ in this mode requires that the migration is run in "online" mode.
+ The ``copy_from`` parameter may be passed which refers to an existing
+ :class:`.Table` object, which will bypass this reflection step.
+
+ .. note:: The table copy operation will currently not copy
+ CHECK constraints, and may not copy UNIQUE constraints that are
+ unnamed, as is possible on SQLite. See the section
+ :ref:`sqlite_batch_constraints` for workarounds.
+
+ :param table_name: name of table
+ :param schema: optional schema name.
+ :param recreate: under what circumstances the table should be
+ recreated. At its default of ``"auto"``, the SQLite dialect will
+ recreate the table if any operations other than ``add_column()``,
+ ``create_index()``, or ``drop_index()`` are
+ present. Other options include ``"always"`` and ``"never"``.
+ :param copy_from: optional :class:`~sqlalchemy.schema.Table` object
+ that will act as the structure of the table being copied. If omitted,
+ table reflection is used to retrieve the structure of the table.
+
+ .. seealso::
+
+ :ref:`batch_offline_mode`
+
+ :paramref:`~.Operations.batch_alter_table.reflect_args`
+
+ :paramref:`~.Operations.batch_alter_table.reflect_kwargs`
+
+ :param reflect_args: a sequence of additional positional arguments that
+ will be applied to the table structure being reflected / copied;
+ this may be used to pass column and constraint overrides to the
+ table that will be reflected, in lieu of passing the whole
+ :class:`~sqlalchemy.schema.Table` using
+ :paramref:`~.Operations.batch_alter_table.copy_from`.
+ :param reflect_kwargs: a dictionary of additional keyword arguments
+ that will be applied to the table structure being copied; this may be
+ used to pass additional table and reflection options to the table that
+ will be reflected, in lieu of passing the whole
+ :class:`~sqlalchemy.schema.Table` using
+ :paramref:`~.Operations.batch_alter_table.copy_from`.
+ :param table_args: a sequence of additional positional arguments that
+ will be applied to the new :class:`~sqlalchemy.schema.Table` when
+ created, in addition to those copied from the source table.
+ This may be used to provide additional constraints such as CHECK
+ constraints that may not be reflected.
+ :param table_kwargs: a dictionary of additional keyword arguments
+ that will be applied to the new :class:`~sqlalchemy.schema.Table`
+ when created, in addition to those copied from the source table.
+ This may be used to provide for additional table options that may
+ not be reflected.
+ :param naming_convention: a naming convention dictionary of the form
+ described at :ref:`autogen_naming_conventions` which will be applied
+ to the :class:`~sqlalchemy.schema.MetaData` during the reflection
+ process. This is typically required if one wants to drop SQLite
+ constraints, as these constraints will not have names when
+ reflected on this backend. Requires SQLAlchemy **0.9.4** or greater.
+
+ .. seealso::
+
+ :ref:`dropping_sqlite_foreign_keys`
+
+ :param partial_reordering: a list of tuples, each suggesting a desired
+ ordering of two or more columns in the newly created table. Requires
+ that :paramref:`.batch_alter_table.recreate` is set to ``"always"``.
+ Examples, given a table with columns "a", "b", "c", and "d":
+
+ Specify the order of all columns::
+
+ with op.batch_alter_table(
+ "some_table",
+ recreate="always",
+ partial_reordering=[("c", "d", "a", "b")],
+ ) as batch_op:
+ pass
+
+ Ensure "d" appears before "c", and "b", appears before "a"::
+
+ with op.batch_alter_table(
+ "some_table",
+ recreate="always",
+ partial_reordering=[("d", "c"), ("b", "a")],
+ ) as batch_op:
+ pass
+
+ The ordering of columns not included in the partial_reordering
+ set is undefined. Therefore it is best to specify the complete
+ ordering of all columns for best results.
+
+ .. note:: batch mode requires SQLAlchemy 0.8 or above.
+
+ .. seealso::
+
+ :ref:`batch_migrations`
+
+ """
+
+def bulk_insert(
+ table: Union[Table, TableClause],
+ rows: List[Dict[str, Any]],
+ *,
+ multiinsert: bool = True,
+) -> None:
+ """Issue a "bulk insert" operation using the current
+ migration context.
+
+ This provides a means of representing an INSERT of multiple rows
+ which works equally well in the context of executing on a live
+ connection as well as that of generating a SQL script. In the
+ case of a SQL script, the values are rendered inline into the
+ statement.
+
+ e.g.::
+
+ from alembic import op
+ from datetime import date
+ from sqlalchemy.sql import table, column
+ from sqlalchemy import String, Integer, Date
+
+ # Create an ad-hoc table to use for the insert statement.
+ accounts_table = table(
+ "account",
+ column("id", Integer),
+ column("name", String),
+ column("create_date", Date),
+ )
+
+ op.bulk_insert(
+ accounts_table,
+ [
+ {
+ "id": 1,
+ "name": "John Smith",
+ "create_date": date(2010, 10, 5),
+ },
+ {
+ "id": 2,
+ "name": "Ed Williams",
+ "create_date": date(2007, 5, 27),
+ },
+ {
+ "id": 3,
+ "name": "Wendy Jones",
+ "create_date": date(2008, 8, 15),
+ },
+ ],
+ )
+
+ When using --sql mode, some datatypes may not render inline
+ automatically, such as dates and other special types. When this
+ issue is present, :meth:`.Operations.inline_literal` may be used::
+
+ op.bulk_insert(
+ accounts_table,
+ [
+ {
+ "id": 1,
+ "name": "John Smith",
+ "create_date": op.inline_literal("2010-10-05"),
+ },
+ {
+ "id": 2,
+ "name": "Ed Williams",
+ "create_date": op.inline_literal("2007-05-27"),
+ },
+ {
+ "id": 3,
+ "name": "Wendy Jones",
+ "create_date": op.inline_literal("2008-08-15"),
+ },
+ ],
+ multiinsert=False,
+ )
+
+ When using :meth:`.Operations.inline_literal` in conjunction with
+ :meth:`.Operations.bulk_insert`, in order for the statement to work
+ in "online" (e.g. non --sql) mode, the
+ :paramref:`~.Operations.bulk_insert.multiinsert`
+ flag should be set to ``False``, which will have the effect of
+ individual INSERT statements being emitted to the database, each
+ with a distinct VALUES clause, so that the "inline" values can
+ still be rendered, rather than attempting to pass the values
+ as bound parameters.
+
+ :param table: a table object which represents the target of the INSERT.
+
+ :param rows: a list of dictionaries indicating rows.
+
+ :param multiinsert: when at its default of True and --sql mode is not
+ enabled, the INSERT statement will be executed using
+ "executemany()" style, where all elements in the list of
+ dictionaries are passed as bound parameters in a single
+ list. Setting this to False results in individual INSERT
+ statements being emitted per parameter set, and is needed
+ in those cases where non-literal values are present in the
+ parameter sets.
+
+ """
+
+def create_check_constraint(
+ constraint_name: Optional[str],
+ table_name: str,
+ condition: Union[str, ColumnElement[bool], TextClause],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+) -> None:
+ """Issue a "create check constraint" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+ from sqlalchemy.sql import column, func
+
+ op.create_check_constraint(
+ "ck_user_name_len",
+ "user",
+ func.len(column("name")) > 5,
+ )
+
+ CHECK constraints are usually against a SQL expression, so ad-hoc
+ table metadata is usually needed. The function will convert the given
+ arguments into a :class:`sqlalchemy.schema.CheckConstraint` bound
+ to an anonymous table in order to emit the CREATE statement.
+
+ :param name: Name of the check constraint. The name is necessary
+ so that an ALTER statement can be emitted. For setups that
+ use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the source table.
+ :param condition: SQL expression that's the condition of the
+ constraint. Can be a string or SQLAlchemy expression language
+ structure.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def create_exclude_constraint(
+ constraint_name: str, table_name: str, *elements: Any, **kw: Any
+) -> Optional[Table]:
+ """Issue an alter to create an EXCLUDE constraint using the
+ current migration context.
+
+ .. note:: This method is Postgresql specific, and additionally
+ requires at least SQLAlchemy 1.0.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_exclude_constraint(
+ "user_excl",
+ "user",
+ ("period", "&&"),
+ ("group", "="),
+ where=("group != 'some group'"),
+ )
+
+ Note that the expressions work the same way as that of
+ the ``ExcludeConstraint`` object itself; if plain strings are
+ passed, quoting rules must be applied manually.
+
+ :param name: Name of the constraint.
+ :param table_name: String name of the source table.
+ :param elements: exclude conditions.
+ :param where: SQL expression or SQL string with optional WHERE
+ clause.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within.
+
+ """
+
+def create_foreign_key(
+ constraint_name: Optional[str],
+ source_table: str,
+ referent_table: str,
+ local_cols: List[str],
+ remote_cols: List[str],
+ *,
+ onupdate: Optional[str] = None,
+ ondelete: Optional[str] = None,
+ deferrable: Optional[bool] = None,
+ initially: Optional[str] = None,
+ match: Optional[str] = None,
+ source_schema: Optional[str] = None,
+ referent_schema: Optional[str] = None,
+ **dialect_kw: Any,
+) -> None:
+ """Issue a "create foreign key" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_foreign_key(
+ "fk_user_address",
+ "address",
+ "user",
+ ["user_id"],
+ ["id"],
+ )
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.ForeignKeyConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param constraint_name: Name of the foreign key constraint. The name
+ is necessary so that an ALTER statement can be emitted. For setups
+ that use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param source_table: String name of the source table.
+ :param referent_table: String name of the destination table.
+ :param local_cols: a list of string column names in the
+ source table.
+ :param remote_cols: a list of string column names in the
+ remote table.
+ :param onupdate: Optional string. If set, emit ON UPDATE when
+ issuing DDL for this constraint. Typical values include CASCADE,
+ DELETE and RESTRICT.
+ :param ondelete: Optional string. If set, emit ON DELETE when
+ issuing DDL for this constraint. Typical values include CASCADE,
+ DELETE and RESTRICT.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or NOT
+ DEFERRABLE when issuing DDL for this constraint.
+ :param source_schema: Optional schema name of the source table.
+ :param referent_schema: Optional schema name of the destination table.
+
+ """
+
+def create_index(
+ index_name: Optional[str],
+ table_name: str,
+ columns: Sequence[Union[str, TextClause, Function[Any]]],
+ *,
+ schema: Optional[str] = None,
+ unique: bool = False,
+ if_not_exists: Optional[bool] = None,
+ **kw: Any,
+) -> None:
+ r"""Issue a "create index" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_index("ik_test", "t1", ["foo", "bar"])
+
+ Functional indexes can be produced by using the
+ :func:`sqlalchemy.sql.expression.text` construct::
+
+ from alembic import op
+ from sqlalchemy import text
+
+ op.create_index("ik_test", "t1", [text("lower(foo)")])
+
+ :param index_name: name of the index.
+ :param table_name: name of the owning table.
+ :param columns: a list consisting of string column names and/or
+ :func:`~sqlalchemy.sql.expression.text` constructs.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param unique: If True, create a unique index.
+
+ :param quote: Force quoting of this column's name on or off,
+ corresponding to ``True`` or ``False``. When left at its default
+ of ``None``, the column identifier will be quoted according to
+ whether the name is case sensitive (identifiers with at least one
+ upper case character are treated as case sensitive), or if it's a
+ reserved word. This flag is only needed to force quoting of a
+ reserved word which is not known by the SQLAlchemy dialect.
+
+ :param if_not_exists: If True, adds IF NOT EXISTS operator when
+ creating the new index.
+
+ .. versionadded:: 1.12.0
+
+ :param \**kw: Additional keyword arguments not mentioned above are
+ dialect specific, and passed in the form
+ ``_``.
+ See the documentation regarding an individual dialect at
+ :ref:`dialect_toplevel` for detail on documented arguments.
+
+ """
+
+def create_primary_key(
+ constraint_name: Optional[str],
+ table_name: str,
+ columns: List[str],
+ *,
+ schema: Optional[str] = None,
+) -> None:
+ """Issue a "create primary key" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_primary_key("pk_my_table", "my_table", ["id", "version"])
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.PrimaryKeyConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param constraint_name: Name of the primary key constraint. The name
+ is necessary so that an ALTER statement can be emitted. For setups
+ that use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the target table.
+ :param columns: a list of string column names to be applied to the
+ primary key constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def create_table(table_name: str, *columns: SchemaItem, **kw: Any) -> Table:
+ r"""Issue a "create table" instruction using the current migration
+ context.
+
+ This directive receives an argument list similar to that of the
+ traditional :class:`sqlalchemy.schema.Table` construct, but without the
+ metadata::
+
+ from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+ from alembic import op
+
+ op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("name", VARCHAR(50), nullable=False),
+ Column("description", NVARCHAR(200)),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ Note that :meth:`.create_table` accepts
+ :class:`~sqlalchemy.schema.Column`
+ constructs directly from the SQLAlchemy library. In particular,
+ default values to be created on the database side are
+ specified using the ``server_default`` parameter, and not
+ ``default`` which only specifies Python-side defaults::
+
+ from alembic import op
+ from sqlalchemy import Column, TIMESTAMP, func
+
+ # specify "DEFAULT NOW" along with the "timestamp" column
+ op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ The function also returns a newly created
+ :class:`~sqlalchemy.schema.Table` object, corresponding to the table
+ specification given, which is suitable for
+ immediate SQL operations, in particular
+ :meth:`.Operations.bulk_insert`::
+
+ from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+ from alembic import op
+
+ account_table = op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("name", VARCHAR(50), nullable=False),
+ Column("description", NVARCHAR(200)),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ op.bulk_insert(
+ account_table,
+ [
+ {"name": "A1", "description": "account 1"},
+ {"name": "A2", "description": "account 2"},
+ ],
+ )
+
+ :param table_name: Name of the table
+ :param \*columns: collection of :class:`~sqlalchemy.schema.Column`
+ objects within
+ the table, as well as optional :class:`~sqlalchemy.schema.Constraint`
+ objects
+ and :class:`~.sqlalchemy.schema.Index` objects.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param \**kw: Other keyword arguments are passed to the underlying
+ :class:`sqlalchemy.schema.Table` object created for the command.
+
+ :return: the :class:`~sqlalchemy.schema.Table` object corresponding
+ to the parameters given.
+
+ """
+
+def create_table_comment(
+ table_name: str,
+ comment: Optional[str],
+ *,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+) -> None:
+ """Emit a COMMENT ON operation to set the comment for a table.
+
+ :param table_name: string name of the target table.
+ :param comment: string value of the comment being registered against
+ the specified table.
+ :param existing_comment: String value of a comment
+ already registered on the specified table, used within autogenerate
+ so that the operation is reversible, but not required for direct
+ use.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_table_comment`
+
+ :paramref:`.Operations.alter_column.comment`
+
+ """
+
+def create_unique_constraint(
+ constraint_name: Optional[str],
+ table_name: str,
+ columns: Sequence[str],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+) -> Any:
+ """Issue a "create unique constraint" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+ op.create_unique_constraint("uq_user_name", "user", ["name"])
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.UniqueConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param name: Name of the unique constraint. The name is necessary
+ so that an ALTER statement can be emitted. For setups that
+ use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the source table.
+ :param columns: a list of string column names in the
+ source table.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def drop_column(
+ table_name: str,
+ column_name: str,
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+) -> None:
+ """Issue a "drop column" instruction using the current
+ migration context.
+
+ e.g.::
+
+ drop_column("organization", "account_id")
+
+ :param table_name: name of table
+ :param column_name: name of column
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param mssql_drop_check: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop the CHECK constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from sys.check_constraints,
+ then exec's a separate DROP CONSTRAINT for that constraint.
+ :param mssql_drop_default: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop the DEFAULT constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from sys.default_constraints,
+ then exec's a separate DROP CONSTRAINT for that default.
+ :param mssql_drop_foreign_key: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop a single FOREIGN KEY constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from
+ sys.foreign_keys/sys.foreign_key_columns,
+ then exec's a separate DROP CONSTRAINT for that default. Only
+ works if the column has exactly one FK constraint which refers to
+ it, at the moment.
+
+ """
+
+def drop_constraint(
+ constraint_name: str,
+ table_name: str,
+ type_: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+) -> None:
+ r"""Drop a constraint of the given name, typically via DROP CONSTRAINT.
+
+ :param constraint_name: name of the constraint.
+ :param table_name: table name.
+ :param type\_: optional, required on MySQL. can be
+ 'foreignkey', 'primary', 'unique', or 'check'.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def drop_index(
+ index_name: str,
+ table_name: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+ if_exists: Optional[bool] = None,
+ **kw: Any,
+) -> None:
+ r"""Issue a "drop index" instruction using the current
+ migration context.
+
+ e.g.::
+
+ drop_index("accounts")
+
+ :param index_name: name of the index.
+ :param table_name: name of the owning table. Some
+ backends such as Microsoft SQL Server require this.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ :param if_exists: If True, adds IF EXISTS operator when
+ dropping the index.
+
+ .. versionadded:: 1.12.0
+
+ :param \**kw: Additional keyword arguments not mentioned above are
+ dialect specific, and passed in the form
+ ``_``.
+ See the documentation regarding an individual dialect at
+ :ref:`dialect_toplevel` for detail on documented arguments.
+
+ """
+
+def drop_table(
+ table_name: str, *, schema: Optional[str] = None, **kw: Any
+) -> None:
+ r"""Issue a "drop table" instruction using the current
+ migration context.
+
+
+ e.g.::
+
+ drop_table("accounts")
+
+ :param table_name: Name of the table
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param \**kw: Other keyword arguments are passed to the underlying
+ :class:`sqlalchemy.schema.Table` object created for the command.
+
+ """
+
+def drop_table_comment(
+ table_name: str,
+ *,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+) -> None:
+ """Issue a "drop table comment" operation to
+ remove an existing comment set on a table.
+
+ :param table_name: string name of the target table.
+ :param existing_comment: An optional string value of a comment already
+ registered on the specified table.
+
+ .. seealso::
+
+ :meth:`.Operations.create_table_comment`
+
+ :paramref:`.Operations.alter_column.comment`
+
+ """
+
+def execute(
+ sqltext: Union[Executable, str],
+ *,
+ execution_options: Optional[dict[str, Any]] = None,
+) -> None:
+ r"""Execute the given SQL using the current migration context.
+
+ The given SQL can be a plain string, e.g.::
+
+ op.execute("INSERT INTO table (foo) VALUES ('some value')")
+
+ Or it can be any kind of Core SQL Expression construct, such as
+ below where we use an update construct::
+
+ from sqlalchemy.sql import table, column
+ from sqlalchemy import String
+ from alembic import op
+
+ account = table("account", column("name", String))
+ op.execute(
+ account.update()
+ .where(account.c.name == op.inline_literal("account 1"))
+ .values({"name": op.inline_literal("account 2")})
+ )
+
+ Above, we made use of the SQLAlchemy
+ :func:`sqlalchemy.sql.expression.table` and
+ :func:`sqlalchemy.sql.expression.column` constructs to make a brief,
+ ad-hoc table construct just for our UPDATE statement. A full
+ :class:`~sqlalchemy.schema.Table` construct of course works perfectly
+ fine as well, though note it's a recommended practice to at least
+ ensure the definition of a table is self-contained within the migration
+ script, rather than imported from a module that may break compatibility
+ with older migrations.
+
+ In a SQL script context, the statement is emitted directly to the
+ output stream. There is *no* return result, however, as this
+ function is oriented towards generating a change script
+ that can run in "offline" mode. Additionally, parameterized
+ statements are discouraged here, as they *will not work* in offline
+ mode. Above, we use :meth:`.inline_literal` where parameters are
+ to be used.
+
+ For full interaction with a connected database where parameters can
+ also be used normally, use the "bind" available from the context::
+
+ from alembic import op
+
+ connection = op.get_bind()
+
+ connection.execute(
+ account.update()
+ .where(account.c.name == "account 1")
+ .values({"name": "account 2"})
+ )
+
+ Additionally, when passing the statement as a plain string, it is first
+ coerced into a :func:`sqlalchemy.sql.expression.text` construct
+ before being passed along. In the less likely case that the
+ literal SQL string contains a colon, it must be escaped with a
+ backslash, as::
+
+ op.execute(r"INSERT INTO table (foo) VALUES ('\:colon_value')")
+
+
+ :param sqltext: Any legal SQLAlchemy expression, including:
+
+ * a string
+ * a :func:`sqlalchemy.sql.expression.text` construct.
+ * a :func:`sqlalchemy.sql.expression.insert` construct.
+ * a :func:`sqlalchemy.sql.expression.update` construct.
+ * a :func:`sqlalchemy.sql.expression.delete` construct.
+ * Any "executable" described in SQLAlchemy Core documentation,
+ noting that no result set is returned.
+
+ .. note:: when passing a plain string, the statement is coerced into
+ a :func:`sqlalchemy.sql.expression.text` construct. This construct
+ considers symbols with colons, e.g. ``:foo`` to be bound parameters.
+ To avoid this, ensure that colon symbols are escaped, e.g.
+ ``\:foo``.
+
+ :param execution_options: Optional dictionary of
+ execution options, will be passed to
+ :meth:`sqlalchemy.engine.Connection.execution_options`.
+ """
+
+def f(name: str) -> conv:
+ """Indicate a string name that has already had a naming convention
+ applied to it.
+
+ This feature combines with the SQLAlchemy ``naming_convention`` feature
+ to disambiguate constraint names that have already had naming
+ conventions applied to them, versus those that have not. This is
+ necessary in the case that the ``"%(constraint_name)s"`` token
+ is used within a naming convention, so that it can be identified
+ that this particular name should remain fixed.
+
+ If the :meth:`.Operations.f` is used on a constraint, the naming
+ convention will not take effect::
+
+ op.add_column("t", "x", Boolean(name=op.f("ck_bool_t_x")))
+
+ Above, the CHECK constraint generated will have the name
+ ``ck_bool_t_x`` regardless of whether or not a naming convention is
+ in use.
+
+ Alternatively, if a naming convention is in use, and 'f' is not used,
+ names will be converted along conventions. If the ``target_metadata``
+ contains the naming convention
+ ``{"ck": "ck_bool_%(table_name)s_%(constraint_name)s"}``, then the
+ output of the following:
+
+ op.add_column("t", "x", Boolean(name="x"))
+
+ will be::
+
+ CONSTRAINT ck_bool_t_x CHECK (x in (1, 0)))
+
+ The function is rendered in the output of autogenerate when
+ a particular constraint name is already converted.
+
+ """
+
+def get_bind() -> Connection:
+ """Return the current 'bind'.
+
+ Under normal circumstances, this is the
+ :class:`~sqlalchemy.engine.Connection` currently being used
+ to emit SQL to the database.
+
+ In a SQL script context, this value is ``None``. [TODO: verify this]
+
+ """
+
+def get_context() -> MigrationContext:
+ """Return the :class:`.MigrationContext` object that's
+ currently in use.
+
+ """
+
+def implementation_for(op_cls: Any) -> Callable[[_C], _C]:
+ """Register an implementation for a given :class:`.MigrateOperation`.
+
+ This is part of the operation extensibility API.
+
+ .. seealso::
+
+ :ref:`operation_plugins` - example of use
+
+ """
+
+def inline_literal(
+ value: Union[str, int], type_: Optional[TypeEngine[Any]] = None
+) -> _literal_bindparam:
+ r"""Produce an 'inline literal' expression, suitable for
+ using in an INSERT, UPDATE, or DELETE statement.
+
+ When using Alembic in "offline" mode, CRUD operations
+ aren't compatible with SQLAlchemy's default behavior surrounding
+ literal values,
+ which is that they are converted into bound values and passed
+ separately into the ``execute()`` method of the DBAPI cursor.
+ An offline SQL
+ script needs to have these rendered inline. While it should
+ always be noted that inline literal values are an **enormous**
+ security hole in an application that handles untrusted input,
+ a schema migration is not run in this context, so
+ literals are safe to render inline, with the caveat that
+ advanced types like dates may not be supported directly
+ by SQLAlchemy.
+
+ See :meth:`.Operations.execute` for an example usage of
+ :meth:`.Operations.inline_literal`.
+
+ The environment can also be configured to attempt to render
+ "literal" values inline automatically, for those simple types
+ that are supported by the dialect; see
+ :paramref:`.EnvironmentContext.configure.literal_binds` for this
+ more recently added feature.
+
+ :param value: The value to render. Strings, integers, and simple
+ numerics should be supported. Other types like boolean,
+ dates, etc. may or may not be supported yet by various
+ backends.
+ :param type\_: optional - a :class:`sqlalchemy.types.TypeEngine`
+ subclass stating the type of this value. In SQLAlchemy
+ expressions, this is usually derived automatically
+ from the Python type of the value itself, as well as
+ based on the context in which the value is used.
+
+ .. seealso::
+
+ :paramref:`.EnvironmentContext.configure.literal_binds`
+
+ """
+
+@overload
+def invoke(operation: CreateTableOp) -> Table: ...
+@overload
+def invoke(
+ operation: Union[
+ AddConstraintOp,
+ DropConstraintOp,
+ CreateIndexOp,
+ DropIndexOp,
+ AddColumnOp,
+ AlterColumnOp,
+ AlterTableOp,
+ CreateTableCommentOp,
+ DropTableCommentOp,
+ DropColumnOp,
+ BulkInsertOp,
+ DropTableOp,
+ ExecuteSQLOp,
+ ]
+) -> None: ...
+@overload
+def invoke(operation: MigrateOperation) -> Any:
+ """Given a :class:`.MigrateOperation`, invoke it in terms of
+ this :class:`.Operations` instance.
+
+ """
+
+def register_operation(
+ name: str, sourcename: Optional[str] = None
+) -> Callable[[Type[_T]], Type[_T]]:
+ """Register a new operation for this class.
+
+ This method is normally used to add new operations
+ to the :class:`.Operations` class, and possibly the
+ :class:`.BatchOperations` class as well. All Alembic migration
+ operations are implemented via this system, however the system
+ is also available as a public API to facilitate adding custom
+ operations.
+
+ .. seealso::
+
+ :ref:`operation_plugins`
+
+
+ """
+
+def rename_table(
+ old_table_name: str, new_table_name: str, *, schema: Optional[str] = None
+) -> None:
+ """Emit an ALTER TABLE to rename a table.
+
+ :param old_table_name: old name.
+ :param new_table_name: new name.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """
+
+def run_async(
+ async_function: Callable[..., Awaitable[_T]], *args: Any, **kw_args: Any
+) -> _T:
+ """Invoke the given asynchronous callable, passing an asynchronous
+ :class:`~sqlalchemy.ext.asyncio.AsyncConnection` as the first
+ argument.
+
+ This method allows calling async functions from within the
+ synchronous ``upgrade()`` or ``downgrade()`` alembic migration
+ method.
+
+ The async connection passed to the callable shares the same
+ transaction as the connection running in the migration context.
+
+ Any additional arg or kw_arg passed to this function are passed
+ to the provided async function.
+
+ .. versionadded: 1.11
+
+ .. note::
+
+ This method can be called only when alembic is called using
+ an async dialect.
+ """
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/operations/__init__.py b/backend/venv/lib/python3.9/site-packages/alembic/operations/__init__.py
new file mode 100644
index 00000000..26197cbe
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/operations/__init__.py
@@ -0,0 +1,15 @@
+from . import toimpl
+from .base import AbstractOperations
+from .base import BatchOperations
+from .base import Operations
+from .ops import MigrateOperation
+from .ops import MigrationScript
+
+
+__all__ = [
+ "AbstractOperations",
+ "Operations",
+ "BatchOperations",
+ "MigrateOperation",
+ "MigrationScript",
+]
diff --git a/backend/venv/lib/python3.9/site-packages/alembic/operations/base.py b/backend/venv/lib/python3.9/site-packages/alembic/operations/base.py
new file mode 100644
index 00000000..bafe441a
--- /dev/null
+++ b/backend/venv/lib/python3.9/site-packages/alembic/operations/base.py
@@ -0,0 +1,1893 @@
+# mypy: allow-untyped-calls
+
+from __future__ import annotations
+
+from contextlib import contextmanager
+import re
+import textwrap
+from typing import Any
+from typing import Awaitable
+from typing import Callable
+from typing import Dict
+from typing import Iterator
+from typing import List # noqa
+from typing import Mapping
+from typing import NoReturn
+from typing import Optional
+from typing import overload
+from typing import Sequence # noqa
+from typing import Tuple
+from typing import Type # noqa
+from typing import TYPE_CHECKING
+from typing import TypeVar
+from typing import Union
+
+from sqlalchemy.sql.elements import conv
+
+from . import batch
+from . import schemaobj
+from .. import util
+from ..util import sqla_compat
+from ..util.compat import formatannotation_fwdref
+from ..util.compat import inspect_formatargspec
+from ..util.compat import inspect_getfullargspec
+from ..util.sqla_compat import _literal_bindparam
+
+
+if TYPE_CHECKING:
+ from typing import Literal
+
+ from sqlalchemy import Table
+ from sqlalchemy.engine import Connection
+ from sqlalchemy.sql import Executable
+ from sqlalchemy.sql.expression import ColumnElement
+ from sqlalchemy.sql.expression import TableClause
+ from sqlalchemy.sql.expression import TextClause
+ from sqlalchemy.sql.functions import Function
+ from sqlalchemy.sql.schema import Column
+ from sqlalchemy.sql.schema import Computed
+ from sqlalchemy.sql.schema import Identity
+ from sqlalchemy.sql.schema import SchemaItem
+ from sqlalchemy.types import TypeEngine
+
+ from .batch import BatchOperationsImpl
+ from .ops import AddColumnOp
+ from .ops import AddConstraintOp
+ from .ops import AlterColumnOp
+ from .ops import AlterTableOp
+ from .ops import BulkInsertOp
+ from .ops import CreateIndexOp
+ from .ops import CreateTableCommentOp
+ from .ops import CreateTableOp
+ from .ops import DropColumnOp
+ from .ops import DropConstraintOp
+ from .ops import DropIndexOp
+ from .ops import DropTableCommentOp
+ from .ops import DropTableOp
+ from .ops import ExecuteSQLOp
+ from .ops import MigrateOperation
+ from ..ddl import DefaultImpl
+ from ..runtime.migration import MigrationContext
+__all__ = ("Operations", "BatchOperations")
+_T = TypeVar("_T")
+
+_C = TypeVar("_C", bound=Callable[..., Any])
+
+
+class AbstractOperations(util.ModuleClsProxy):
+ """Base class for Operations and BatchOperations.
+
+ .. versionadded:: 1.11.0
+
+ """
+
+ impl: Union[DefaultImpl, BatchOperationsImpl]
+ _to_impl = util.Dispatcher()
+
+ def __init__(
+ self,
+ migration_context: MigrationContext,
+ impl: Optional[BatchOperationsImpl] = None,
+ ) -> None:
+ """Construct a new :class:`.Operations`
+
+ :param migration_context: a :class:`.MigrationContext`
+ instance.
+
+ """
+ self.migration_context = migration_context
+ if impl is None:
+ self.impl = migration_context.impl
+ else:
+ self.impl = impl
+
+ self.schema_obj = schemaobj.SchemaObjects(migration_context)
+
+ @classmethod
+ def register_operation(
+ cls, name: str, sourcename: Optional[str] = None
+ ) -> Callable[[Type[_T]], Type[_T]]:
+ """Register a new operation for this class.
+
+ This method is normally used to add new operations
+ to the :class:`.Operations` class, and possibly the
+ :class:`.BatchOperations` class as well. All Alembic migration
+ operations are implemented via this system, however the system
+ is also available as a public API to facilitate adding custom
+ operations.
+
+ .. seealso::
+
+ :ref:`operation_plugins`
+
+
+ """
+
+ def register(op_cls: Type[_T]) -> Type[_T]:
+ if sourcename is None:
+ fn = getattr(op_cls, name)
+ source_name = fn.__name__
+ else:
+ fn = getattr(op_cls, sourcename)
+ source_name = fn.__name__
+
+ spec = inspect_getfullargspec(fn)
+
+ name_args = spec[0]
+ assert name_args[0:2] == ["cls", "operations"]
+
+ name_args[0:2] = ["self"]
+
+ args = inspect_formatargspec(
+ *spec, formatannotation=formatannotation_fwdref
+ )
+ num_defaults = len(spec[3]) if spec[3] else 0
+
+ defaulted_vals: Tuple[Any, ...]
+
+ if num_defaults:
+ defaulted_vals = tuple(name_args[0 - num_defaults :])
+ else:
+ defaulted_vals = ()
+
+ defaulted_vals += tuple(spec[4])
+ # here, we are using formatargspec in a different way in order
+ # to get a string that will re-apply incoming arguments to a new
+ # function call
+
+ apply_kw = inspect_formatargspec(
+ name_args + spec[4],
+ spec[1],
+ spec[2],
+ defaulted_vals,
+ formatvalue=lambda x: "=" + x,
+ formatannotation=formatannotation_fwdref,
+ )
+
+ args = re.sub(
+ r'[_]?ForwardRef\(([\'"].+?[\'"])\)',
+ lambda m: m.group(1),
+ args,
+ )
+
+ func_text = textwrap.dedent(
+ """\
+ def %(name)s%(args)s:
+ %(doc)r
+ return op_cls.%(source_name)s%(apply_kw)s
+ """
+ % {
+ "name": name,
+ "source_name": source_name,
+ "args": args,
+ "apply_kw": apply_kw,
+ "doc": fn.__doc__,
+ }
+ )
+
+ globals_ = dict(globals())
+ globals_.update({"op_cls": op_cls})
+ lcl: Dict[str, Any] = {}
+
+ exec(func_text, globals_, lcl)
+ setattr(cls, name, lcl[name])
+ fn.__func__.__doc__ = (
+ "This method is proxied on "
+ "the :class:`.%s` class, via the :meth:`.%s.%s` method."
+ % (cls.__name__, cls.__name__, name)
+ )
+ if hasattr(fn, "_legacy_translations"):
+ lcl[name]._legacy_translations = fn._legacy_translations
+ return op_cls
+
+ return register
+
+ @classmethod
+ def implementation_for(cls, op_cls: Any) -> Callable[[_C], _C]:
+ """Register an implementation for a given :class:`.MigrateOperation`.
+
+ This is part of the operation extensibility API.
+
+ .. seealso::
+
+ :ref:`operation_plugins` - example of use
+
+ """
+
+ def decorate(fn: _C) -> _C:
+ cls._to_impl.dispatch_for(op_cls)(fn)
+ return fn
+
+ return decorate
+
+ @classmethod
+ @contextmanager
+ def context(
+ cls, migration_context: MigrationContext
+ ) -> Iterator[Operations]:
+ op = Operations(migration_context)
+ op._install_proxy()
+ yield op
+ op._remove_proxy()
+
+ @contextmanager
+ def batch_alter_table(
+ self,
+ table_name: str,
+ schema: Optional[str] = None,
+ recreate: Literal["auto", "always", "never"] = "auto",
+ partial_reordering: Optional[Tuple[Any, ...]] = None,
+ copy_from: Optional[Table] = None,
+ table_args: Tuple[Any, ...] = (),
+ table_kwargs: Mapping[str, Any] = util.immutabledict(),
+ reflect_args: Tuple[Any, ...] = (),
+ reflect_kwargs: Mapping[str, Any] = util.immutabledict(),
+ naming_convention: Optional[Dict[str, str]] = None,
+ ) -> Iterator[BatchOperations]:
+ """Invoke a series of per-table migrations in batch.
+
+ Batch mode allows a series of operations specific to a table
+ to be syntactically grouped together, and allows for alternate
+ modes of table migration, in particular the "recreate" style of
+ migration required by SQLite.
+
+ "recreate" style is as follows:
+
+ 1. A new table is created with the new specification, based on the
+ migration directives within the batch, using a temporary name.
+
+ 2. the data copied from the existing table to the new table.
+
+ 3. the existing table is dropped.
+
+ 4. the new table is renamed to the existing table name.
+
+ The directive by default will only use "recreate" style on the
+ SQLite backend, and only if directives are present which require
+ this form, e.g. anything other than ``add_column()``. The batch
+ operation on other backends will proceed using standard ALTER TABLE
+ operations.
+
+ The method is used as a context manager, which returns an instance
+ of :class:`.BatchOperations`; this object is the same as
+ :class:`.Operations` except that table names and schema names
+ are omitted. E.g.::
+
+ with op.batch_alter_table("some_table") as batch_op:
+ batch_op.add_column(Column("foo", Integer))
+ batch_op.drop_column("bar")
+
+ The operations within the context manager are invoked at once
+ when the context is ended. When run against SQLite, if the
+ migrations include operations not supported by SQLite's ALTER TABLE,
+ the entire table will be copied to a new one with the new
+ specification, moving all data across as well.
+
+ The copy operation by default uses reflection to retrieve the current
+ structure of the table, and therefore :meth:`.batch_alter_table`
+ in this mode requires that the migration is run in "online" mode.
+ The ``copy_from`` parameter may be passed which refers to an existing
+ :class:`.Table` object, which will bypass this reflection step.
+
+ .. note:: The table copy operation will currently not copy
+ CHECK constraints, and may not copy UNIQUE constraints that are
+ unnamed, as is possible on SQLite. See the section
+ :ref:`sqlite_batch_constraints` for workarounds.
+
+ :param table_name: name of table
+ :param schema: optional schema name.
+ :param recreate: under what circumstances the table should be
+ recreated. At its default of ``"auto"``, the SQLite dialect will
+ recreate the table if any operations other than ``add_column()``,
+ ``create_index()``, or ``drop_index()`` are
+ present. Other options include ``"always"`` and ``"never"``.
+ :param copy_from: optional :class:`~sqlalchemy.schema.Table` object
+ that will act as the structure of the table being copied. If omitted,
+ table reflection is used to retrieve the structure of the table.
+
+ .. seealso::
+
+ :ref:`batch_offline_mode`
+
+ :paramref:`~.Operations.batch_alter_table.reflect_args`
+
+ :paramref:`~.Operations.batch_alter_table.reflect_kwargs`
+
+ :param reflect_args: a sequence of additional positional arguments that
+ will be applied to the table structure being reflected / copied;
+ this may be used to pass column and constraint overrides to the
+ table that will be reflected, in lieu of passing the whole
+ :class:`~sqlalchemy.schema.Table` using
+ :paramref:`~.Operations.batch_alter_table.copy_from`.
+ :param reflect_kwargs: a dictionary of additional keyword arguments
+ that will be applied to the table structure being copied; this may be
+ used to pass additional table and reflection options to the table that
+ will be reflected, in lieu of passing the whole
+ :class:`~sqlalchemy.schema.Table` using
+ :paramref:`~.Operations.batch_alter_table.copy_from`.
+ :param table_args: a sequence of additional positional arguments that
+ will be applied to the new :class:`~sqlalchemy.schema.Table` when
+ created, in addition to those copied from the source table.
+ This may be used to provide additional constraints such as CHECK
+ constraints that may not be reflected.
+ :param table_kwargs: a dictionary of additional keyword arguments
+ that will be applied to the new :class:`~sqlalchemy.schema.Table`
+ when created, in addition to those copied from the source table.
+ This may be used to provide for additional table options that may
+ not be reflected.
+ :param naming_convention: a naming convention dictionary of the form
+ described at :ref:`autogen_naming_conventions` which will be applied
+ to the :class:`~sqlalchemy.schema.MetaData` during the reflection
+ process. This is typically required if one wants to drop SQLite
+ constraints, as these constraints will not have names when
+ reflected on this backend. Requires SQLAlchemy **0.9.4** or greater.
+
+ .. seealso::
+
+ :ref:`dropping_sqlite_foreign_keys`
+
+ :param partial_reordering: a list of tuples, each suggesting a desired
+ ordering of two or more columns in the newly created table. Requires
+ that :paramref:`.batch_alter_table.recreate` is set to ``"always"``.
+ Examples, given a table with columns "a", "b", "c", and "d":
+
+ Specify the order of all columns::
+
+ with op.batch_alter_table(
+ "some_table",
+ recreate="always",
+ partial_reordering=[("c", "d", "a", "b")],
+ ) as batch_op:
+ pass
+
+ Ensure "d" appears before "c", and "b", appears before "a"::
+
+ with op.batch_alter_table(
+ "some_table",
+ recreate="always",
+ partial_reordering=[("d", "c"), ("b", "a")],
+ ) as batch_op:
+ pass
+
+ The ordering of columns not included in the partial_reordering
+ set is undefined. Therefore it is best to specify the complete
+ ordering of all columns for best results.
+
+ .. note:: batch mode requires SQLAlchemy 0.8 or above.
+
+ .. seealso::
+
+ :ref:`batch_migrations`
+
+ """
+ impl = batch.BatchOperationsImpl(
+ self,
+ table_name,
+ schema,
+ recreate,
+ copy_from,
+ table_args,
+ table_kwargs,
+ reflect_args,
+ reflect_kwargs,
+ naming_convention,
+ partial_reordering,
+ )
+ batch_op = BatchOperations(self.migration_context, impl=impl)
+ yield batch_op
+ impl.flush()
+
+ def get_context(self) -> MigrationContext:
+ """Return the :class:`.MigrationContext` object that's
+ currently in use.
+
+ """
+
+ return self.migration_context
+
+ @overload
+ def invoke(self, operation: CreateTableOp) -> Table:
+ ...
+
+ @overload
+ def invoke(
+ self,
+ operation: Union[
+ AddConstraintOp,
+ DropConstraintOp,
+ CreateIndexOp,
+ DropIndexOp,
+ AddColumnOp,
+ AlterColumnOp,
+ AlterTableOp,
+ CreateTableCommentOp,
+ DropTableCommentOp,
+ DropColumnOp,
+ BulkInsertOp,
+ DropTableOp,
+ ExecuteSQLOp,
+ ],
+ ) -> None:
+ ...
+
+ @overload
+ def invoke(self, operation: MigrateOperation) -> Any:
+ ...
+
+ def invoke(self, operation: MigrateOperation) -> Any:
+ """Given a :class:`.MigrateOperation`, invoke it in terms of
+ this :class:`.Operations` instance.
+
+ """
+ fn = self._to_impl.dispatch(
+ operation, self.migration_context.impl.__dialect__
+ )
+ return fn(self, operation)
+
+ def f(self, name: str) -> conv:
+ """Indicate a string name that has already had a naming convention
+ applied to it.
+
+ This feature combines with the SQLAlchemy ``naming_convention`` feature
+ to disambiguate constraint names that have already had naming
+ conventions applied to them, versus those that have not. This is
+ necessary in the case that the ``"%(constraint_name)s"`` token
+ is used within a naming convention, so that it can be identified
+ that this particular name should remain fixed.
+
+ If the :meth:`.Operations.f` is used on a constraint, the naming
+ convention will not take effect::
+
+ op.add_column("t", "x", Boolean(name=op.f("ck_bool_t_x")))
+
+ Above, the CHECK constraint generated will have the name
+ ``ck_bool_t_x`` regardless of whether or not a naming convention is
+ in use.
+
+ Alternatively, if a naming convention is in use, and 'f' is not used,
+ names will be converted along conventions. If the ``target_metadata``
+ contains the naming convention
+ ``{"ck": "ck_bool_%(table_name)s_%(constraint_name)s"}``, then the
+ output of the following:
+
+ op.add_column("t", "x", Boolean(name="x"))
+
+ will be::
+
+ CONSTRAINT ck_bool_t_x CHECK (x in (1, 0)))
+
+ The function is rendered in the output of autogenerate when
+ a particular constraint name is already converted.
+
+ """
+ return conv(name)
+
+ def inline_literal(
+ self, value: Union[str, int], type_: Optional[TypeEngine[Any]] = None
+ ) -> _literal_bindparam:
+ r"""Produce an 'inline literal' expression, suitable for
+ using in an INSERT, UPDATE, or DELETE statement.
+
+ When using Alembic in "offline" mode, CRUD operations
+ aren't compatible with SQLAlchemy's default behavior surrounding
+ literal values,
+ which is that they are converted into bound values and passed
+ separately into the ``execute()`` method of the DBAPI cursor.
+ An offline SQL
+ script needs to have these rendered inline. While it should
+ always be noted that inline literal values are an **enormous**
+ security hole in an application that handles untrusted input,
+ a schema migration is not run in this context, so
+ literals are safe to render inline, with the caveat that
+ advanced types like dates may not be supported directly
+ by SQLAlchemy.
+
+ See :meth:`.Operations.execute` for an example usage of
+ :meth:`.Operations.inline_literal`.
+
+ The environment can also be configured to attempt to render
+ "literal" values inline automatically, for those simple types
+ that are supported by the dialect; see
+ :paramref:`.EnvironmentContext.configure.literal_binds` for this
+ more recently added feature.
+
+ :param value: The value to render. Strings, integers, and simple
+ numerics should be supported. Other types like boolean,
+ dates, etc. may or may not be supported yet by various
+ backends.
+ :param type\_: optional - a :class:`sqlalchemy.types.TypeEngine`
+ subclass stating the type of this value. In SQLAlchemy
+ expressions, this is usually derived automatically
+ from the Python type of the value itself, as well as
+ based on the context in which the value is used.
+
+ .. seealso::
+
+ :paramref:`.EnvironmentContext.configure.literal_binds`
+
+ """
+ return sqla_compat._literal_bindparam(None, value, type_=type_)
+
+ def get_bind(self) -> Connection:
+ """Return the current 'bind'.
+
+ Under normal circumstances, this is the
+ :class:`~sqlalchemy.engine.Connection` currently being used
+ to emit SQL to the database.
+
+ In a SQL script context, this value is ``None``. [TODO: verify this]
+
+ """
+ return self.migration_context.impl.bind # type: ignore[return-value]
+
+ def run_async(
+ self,
+ async_function: Callable[..., Awaitable[_T]],
+ *args: Any,
+ **kw_args: Any,
+ ) -> _T:
+ """Invoke the given asynchronous callable, passing an asynchronous
+ :class:`~sqlalchemy.ext.asyncio.AsyncConnection` as the first
+ argument.
+
+ This method allows calling async functions from within the
+ synchronous ``upgrade()`` or ``downgrade()`` alembic migration
+ method.
+
+ The async connection passed to the callable shares the same
+ transaction as the connection running in the migration context.
+
+ Any additional arg or kw_arg passed to this function are passed
+ to the provided async function.
+
+ .. versionadded: 1.11
+
+ .. note::
+
+ This method can be called only when alembic is called using
+ an async dialect.
+ """
+ if not sqla_compat.sqla_14_18:
+ raise NotImplementedError("SQLAlchemy 1.4.18+ required")
+ sync_conn = self.get_bind()
+ if sync_conn is None:
+ raise NotImplementedError("Cannot call run_async in SQL mode")
+ if not sync_conn.dialect.is_async:
+ raise ValueError("Cannot call run_async with a sync engine")
+ from sqlalchemy.ext.asyncio import AsyncConnection
+ from sqlalchemy.util import await_only
+
+ async_conn = AsyncConnection._retrieve_proxy_for_target(sync_conn)
+ return await_only(async_function(async_conn, *args, **kw_args))
+
+
+class Operations(AbstractOperations):
+ """Define high level migration operations.
+
+ Each operation corresponds to some schema migration operation,
+ executed against a particular :class:`.MigrationContext`
+ which in turn represents connectivity to a database,
+ or a file output stream.
+
+ While :class:`.Operations` is normally configured as
+ part of the :meth:`.EnvironmentContext.run_migrations`
+ method called from an ``env.py`` script, a standalone
+ :class:`.Operations` instance can be
+ made for use cases external to regular Alembic
+ migrations by passing in a :class:`.MigrationContext`::
+
+ from alembic.migration import MigrationContext
+ from alembic.operations import Operations
+
+ conn = myengine.connect()
+ ctx = MigrationContext.configure(conn)
+ op = Operations(ctx)
+
+ op.alter_column("t", "c", nullable=True)
+
+ Note that as of 0.8, most of the methods on this class are produced
+ dynamically using the :meth:`.Operations.register_operation`
+ method.
+
+ """
+
+ if TYPE_CHECKING:
+ # START STUB FUNCTIONS: op_cls
+ # ### the following stubs are generated by tools/write_pyi.py ###
+ # ### do not edit ###
+
+ def add_column(
+ self,
+ table_name: str,
+ column: Column[Any],
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Issue an "add column" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+ from sqlalchemy import Column, String
+
+ op.add_column("organization", Column("name", String()))
+
+ The :meth:`.Operations.add_column` method typically corresponds
+ to the SQL command "ALTER TABLE... ADD COLUMN". Within the scope
+ of this command, the column's name, datatype, nullability,
+ and optional server-generated defaults may be indicated.
+
+ .. note::
+
+ With the exception of NOT NULL constraints or single-column FOREIGN
+ KEY constraints, other kinds of constraints such as PRIMARY KEY,
+ UNIQUE or CHECK constraints **cannot** be generated using this
+ method; for these constraints, refer to operations such as
+ :meth:`.Operations.create_primary_key` and
+ :meth:`.Operations.create_check_constraint`. In particular, the
+ following :class:`~sqlalchemy.schema.Column` parameters are
+ **ignored**:
+
+ * :paramref:`~sqlalchemy.schema.Column.primary_key` - SQL databases
+ typically do not support an ALTER operation that can add
+ individual columns one at a time to an existing primary key
+ constraint, therefore it's less ambiguous to use the
+ :meth:`.Operations.create_primary_key` method, which assumes no
+ existing primary key constraint is present.
+ * :paramref:`~sqlalchemy.schema.Column.unique` - use the
+ :meth:`.Operations.create_unique_constraint` method
+ * :paramref:`~sqlalchemy.schema.Column.index` - use the
+ :meth:`.Operations.create_index` method
+
+
+ The provided :class:`~sqlalchemy.schema.Column` object may include a
+ :class:`~sqlalchemy.schema.ForeignKey` constraint directive,
+ referencing a remote table name. For this specific type of constraint,
+ Alembic will automatically emit a second ALTER statement in order to
+ add the single-column FOREIGN KEY constraint separately::
+
+ from alembic import op
+ from sqlalchemy import Column, INTEGER, ForeignKey
+
+ op.add_column(
+ "organization",
+ Column("account_id", INTEGER, ForeignKey("accounts.id")),
+ )
+
+ The column argument passed to :meth:`.Operations.add_column` is a
+ :class:`~sqlalchemy.schema.Column` construct, used in the same way it's
+ used in SQLAlchemy. In particular, values or functions to be indicated
+ as producing the column's default value on the database side are
+ specified using the ``server_default`` parameter, and not ``default``
+ which only specifies Python-side defaults::
+
+ from alembic import op
+ from sqlalchemy import Column, TIMESTAMP, func
+
+ # specify "DEFAULT NOW" along with the column add
+ op.add_column(
+ "account",
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ :param table_name: String name of the parent table.
+ :param column: a :class:`sqlalchemy.schema.Column` object
+ representing the new column.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ def alter_column(
+ self,
+ table_name: str,
+ column_name: str,
+ *,
+ nullable: Optional[bool] = None,
+ comment: Union[str, Literal[False], None] = False,
+ server_default: Any = False,
+ new_column_name: Optional[str] = None,
+ type_: Union[TypeEngine[Any], Type[TypeEngine[Any]], None] = None,
+ existing_type: Union[
+ TypeEngine[Any], Type[TypeEngine[Any]], None
+ ] = None,
+ existing_server_default: Union[
+ str, bool, Identity, Computed, None
+ ] = False,
+ existing_nullable: Optional[bool] = None,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ r"""Issue an "alter column" instruction using the
+ current migration context.
+
+ Generally, only that aspect of the column which
+ is being changed, i.e. name, type, nullability,
+ default, needs to be specified. Multiple changes
+ can also be specified at once and the backend should
+ "do the right thing", emitting each change either
+ separately or together as the backend allows.
+
+ MySQL has special requirements here, since MySQL
+ cannot ALTER a column without a full specification.
+ When producing MySQL-compatible migration files,
+ it is recommended that the ``existing_type``,
+ ``existing_server_default``, and ``existing_nullable``
+ parameters be present, if not being altered.
+
+ Type changes which are against the SQLAlchemy
+ "schema" types :class:`~sqlalchemy.types.Boolean`
+ and :class:`~sqlalchemy.types.Enum` may also
+ add or drop constraints which accompany those
+ types on backends that don't support them natively.
+ The ``existing_type`` argument is
+ used in this case to identify and remove a previous
+ constraint that was bound to the type object.
+
+ :param table_name: string name of the target table.
+ :param column_name: string name of the target column,
+ as it exists before the operation begins.
+ :param nullable: Optional; specify ``True`` or ``False``
+ to alter the column's nullability.
+ :param server_default: Optional; specify a string
+ SQL expression, :func:`~sqlalchemy.sql.expression.text`,
+ or :class:`~sqlalchemy.schema.DefaultClause` to indicate
+ an alteration to the column's default value.
+ Set to ``None`` to have the default removed.
+ :param comment: optional string text of a new comment to add to the
+ column.
+ :param new_column_name: Optional; specify a string name here to
+ indicate the new name within a column rename operation.
+ :param type\_: Optional; a :class:`~sqlalchemy.types.TypeEngine`
+ type object to specify a change to the column's type.
+ For SQLAlchemy types that also indicate a constraint (i.e.
+ :class:`~sqlalchemy.types.Boolean`, :class:`~sqlalchemy.types.Enum`),
+ the constraint is also generated.
+ :param autoincrement: set the ``AUTO_INCREMENT`` flag of the column;
+ currently understood by the MySQL dialect.
+ :param existing_type: Optional; a
+ :class:`~sqlalchemy.types.TypeEngine`
+ type object to specify the previous type. This
+ is required for all MySQL column alter operations that
+ don't otherwise specify a new type, as well as for
+ when nullability is being changed on a SQL Server
+ column. It is also used if the type is a so-called
+ SQLAlchemy "schema" type which may define a constraint (i.e.
+ :class:`~sqlalchemy.types.Boolean`,
+ :class:`~sqlalchemy.types.Enum`),
+ so that the constraint can be dropped.
+ :param existing_server_default: Optional; The existing
+ default value of the column. Required on MySQL if
+ an existing default is not being changed; else MySQL
+ removes the default.
+ :param existing_nullable: Optional; the existing nullability
+ of the column. Required on MySQL if the existing nullability
+ is not being changed; else MySQL sets this to NULL.
+ :param existing_autoincrement: Optional; the existing autoincrement
+ of the column. Used for MySQL's system of altering a column
+ that specifies ``AUTO_INCREMENT``.
+ :param existing_comment: string text of the existing comment on the
+ column to be maintained. Required on MySQL if the existing comment
+ on the column is not being changed.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param postgresql_using: String argument which will indicate a
+ SQL expression to render within the Postgresql-specific USING clause
+ within ALTER COLUMN. This string is taken directly as raw SQL which
+ must explicitly include any necessary quoting or escaping of tokens
+ within the expression.
+
+ """ # noqa: E501
+ ...
+
+ def bulk_insert(
+ self,
+ table: Union[Table, TableClause],
+ rows: List[Dict[str, Any]],
+ *,
+ multiinsert: bool = True,
+ ) -> None:
+ """Issue a "bulk insert" operation using the current
+ migration context.
+
+ This provides a means of representing an INSERT of multiple rows
+ which works equally well in the context of executing on a live
+ connection as well as that of generating a SQL script. In the
+ case of a SQL script, the values are rendered inline into the
+ statement.
+
+ e.g.::
+
+ from alembic import op
+ from datetime import date
+ from sqlalchemy.sql import table, column
+ from sqlalchemy import String, Integer, Date
+
+ # Create an ad-hoc table to use for the insert statement.
+ accounts_table = table(
+ "account",
+ column("id", Integer),
+ column("name", String),
+ column("create_date", Date),
+ )
+
+ op.bulk_insert(
+ accounts_table,
+ [
+ {
+ "id": 1,
+ "name": "John Smith",
+ "create_date": date(2010, 10, 5),
+ },
+ {
+ "id": 2,
+ "name": "Ed Williams",
+ "create_date": date(2007, 5, 27),
+ },
+ {
+ "id": 3,
+ "name": "Wendy Jones",
+ "create_date": date(2008, 8, 15),
+ },
+ ],
+ )
+
+ When using --sql mode, some datatypes may not render inline
+ automatically, such as dates and other special types. When this
+ issue is present, :meth:`.Operations.inline_literal` may be used::
+
+ op.bulk_insert(
+ accounts_table,
+ [
+ {
+ "id": 1,
+ "name": "John Smith",
+ "create_date": op.inline_literal("2010-10-05"),
+ },
+ {
+ "id": 2,
+ "name": "Ed Williams",
+ "create_date": op.inline_literal("2007-05-27"),
+ },
+ {
+ "id": 3,
+ "name": "Wendy Jones",
+ "create_date": op.inline_literal("2008-08-15"),
+ },
+ ],
+ multiinsert=False,
+ )
+
+ When using :meth:`.Operations.inline_literal` in conjunction with
+ :meth:`.Operations.bulk_insert`, in order for the statement to work
+ in "online" (e.g. non --sql) mode, the
+ :paramref:`~.Operations.bulk_insert.multiinsert`
+ flag should be set to ``False``, which will have the effect of
+ individual INSERT statements being emitted to the database, each
+ with a distinct VALUES clause, so that the "inline" values can
+ still be rendered, rather than attempting to pass the values
+ as bound parameters.
+
+ :param table: a table object which represents the target of the INSERT.
+
+ :param rows: a list of dictionaries indicating rows.
+
+ :param multiinsert: when at its default of True and --sql mode is not
+ enabled, the INSERT statement will be executed using
+ "executemany()" style, where all elements in the list of
+ dictionaries are passed as bound parameters in a single
+ list. Setting this to False results in individual INSERT
+ statements being emitted per parameter set, and is needed
+ in those cases where non-literal values are present in the
+ parameter sets.
+
+ """ # noqa: E501
+ ...
+
+ def create_check_constraint(
+ self,
+ constraint_name: Optional[str],
+ table_name: str,
+ condition: Union[str, ColumnElement[bool], TextClause],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ """Issue a "create check constraint" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+ from sqlalchemy.sql import column, func
+
+ op.create_check_constraint(
+ "ck_user_name_len",
+ "user",
+ func.len(column("name")) > 5,
+ )
+
+ CHECK constraints are usually against a SQL expression, so ad-hoc
+ table metadata is usually needed. The function will convert the given
+ arguments into a :class:`sqlalchemy.schema.CheckConstraint` bound
+ to an anonymous table in order to emit the CREATE statement.
+
+ :param name: Name of the check constraint. The name is necessary
+ so that an ALTER statement can be emitted. For setups that
+ use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the source table.
+ :param condition: SQL expression that's the condition of the
+ constraint. Can be a string or SQLAlchemy expression language
+ structure.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ def create_exclude_constraint(
+ self,
+ constraint_name: str,
+ table_name: str,
+ *elements: Any,
+ **kw: Any,
+ ) -> Optional[Table]:
+ """Issue an alter to create an EXCLUDE constraint using the
+ current migration context.
+
+ .. note:: This method is Postgresql specific, and additionally
+ requires at least SQLAlchemy 1.0.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_exclude_constraint(
+ "user_excl",
+ "user",
+ ("period", "&&"),
+ ("group", "="),
+ where=("group != 'some group'"),
+ )
+
+ Note that the expressions work the same way as that of
+ the ``ExcludeConstraint`` object itself; if plain strings are
+ passed, quoting rules must be applied manually.
+
+ :param name: Name of the constraint.
+ :param table_name: String name of the source table.
+ :param elements: exclude conditions.
+ :param where: SQL expression or SQL string with optional WHERE
+ clause.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within.
+
+ """ # noqa: E501
+ ...
+
+ def create_foreign_key(
+ self,
+ constraint_name: Optional[str],
+ source_table: str,
+ referent_table: str,
+ local_cols: List[str],
+ remote_cols: List[str],
+ *,
+ onupdate: Optional[str] = None,
+ ondelete: Optional[str] = None,
+ deferrable: Optional[bool] = None,
+ initially: Optional[str] = None,
+ match: Optional[str] = None,
+ source_schema: Optional[str] = None,
+ referent_schema: Optional[str] = None,
+ **dialect_kw: Any,
+ ) -> None:
+ """Issue a "create foreign key" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_foreign_key(
+ "fk_user_address",
+ "address",
+ "user",
+ ["user_id"],
+ ["id"],
+ )
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.ForeignKeyConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param constraint_name: Name of the foreign key constraint. The name
+ is necessary so that an ALTER statement can be emitted. For setups
+ that use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param source_table: String name of the source table.
+ :param referent_table: String name of the destination table.
+ :param local_cols: a list of string column names in the
+ source table.
+ :param remote_cols: a list of string column names in the
+ remote table.
+ :param onupdate: Optional string. If set, emit ON UPDATE when
+ issuing DDL for this constraint. Typical values include CASCADE,
+ DELETE and RESTRICT.
+ :param ondelete: Optional string. If set, emit ON DELETE when
+ issuing DDL for this constraint. Typical values include CASCADE,
+ DELETE and RESTRICT.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or NOT
+ DEFERRABLE when issuing DDL for this constraint.
+ :param source_schema: Optional schema name of the source table.
+ :param referent_schema: Optional schema name of the destination table.
+
+ """ # noqa: E501
+ ...
+
+ def create_index(
+ self,
+ index_name: Optional[str],
+ table_name: str,
+ columns: Sequence[Union[str, TextClause, Function[Any]]],
+ *,
+ schema: Optional[str] = None,
+ unique: bool = False,
+ if_not_exists: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ r"""Issue a "create index" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_index("ik_test", "t1", ["foo", "bar"])
+
+ Functional indexes can be produced by using the
+ :func:`sqlalchemy.sql.expression.text` construct::
+
+ from alembic import op
+ from sqlalchemy import text
+
+ op.create_index("ik_test", "t1", [text("lower(foo)")])
+
+ :param index_name: name of the index.
+ :param table_name: name of the owning table.
+ :param columns: a list consisting of string column names and/or
+ :func:`~sqlalchemy.sql.expression.text` constructs.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param unique: If True, create a unique index.
+
+ :param quote: Force quoting of this column's name on or off,
+ corresponding to ``True`` or ``False``. When left at its default
+ of ``None``, the column identifier will be quoted according to
+ whether the name is case sensitive (identifiers with at least one
+ upper case character are treated as case sensitive), or if it's a
+ reserved word. This flag is only needed to force quoting of a
+ reserved word which is not known by the SQLAlchemy dialect.
+
+ :param if_not_exists: If True, adds IF NOT EXISTS operator when
+ creating the new index.
+
+ .. versionadded:: 1.12.0
+
+ :param \**kw: Additional keyword arguments not mentioned above are
+ dialect specific, and passed in the form
+ ``_``.
+ See the documentation regarding an individual dialect at
+ :ref:`dialect_toplevel` for detail on documented arguments.
+
+ """ # noqa: E501
+ ...
+
+ def create_primary_key(
+ self,
+ constraint_name: Optional[str],
+ table_name: str,
+ columns: List[str],
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Issue a "create primary key" instruction using the current
+ migration context.
+
+ e.g.::
+
+ from alembic import op
+
+ op.create_primary_key("pk_my_table", "my_table", ["id", "version"])
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.PrimaryKeyConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param constraint_name: Name of the primary key constraint. The name
+ is necessary so that an ALTER statement can be emitted. For setups
+ that use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the target table.
+ :param columns: a list of string column names to be applied to the
+ primary key constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ def create_table(
+ self, table_name: str, *columns: SchemaItem, **kw: Any
+ ) -> Table:
+ r"""Issue a "create table" instruction using the current migration
+ context.
+
+ This directive receives an argument list similar to that of the
+ traditional :class:`sqlalchemy.schema.Table` construct, but without the
+ metadata::
+
+ from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+ from alembic import op
+
+ op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("name", VARCHAR(50), nullable=False),
+ Column("description", NVARCHAR(200)),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ Note that :meth:`.create_table` accepts
+ :class:`~sqlalchemy.schema.Column`
+ constructs directly from the SQLAlchemy library. In particular,
+ default values to be created on the database side are
+ specified using the ``server_default`` parameter, and not
+ ``default`` which only specifies Python-side defaults::
+
+ from alembic import op
+ from sqlalchemy import Column, TIMESTAMP, func
+
+ # specify "DEFAULT NOW" along with the "timestamp" column
+ op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ The function also returns a newly created
+ :class:`~sqlalchemy.schema.Table` object, corresponding to the table
+ specification given, which is suitable for
+ immediate SQL operations, in particular
+ :meth:`.Operations.bulk_insert`::
+
+ from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+ from alembic import op
+
+ account_table = op.create_table(
+ "account",
+ Column("id", INTEGER, primary_key=True),
+ Column("name", VARCHAR(50), nullable=False),
+ Column("description", NVARCHAR(200)),
+ Column("timestamp", TIMESTAMP, server_default=func.now()),
+ )
+
+ op.bulk_insert(
+ account_table,
+ [
+ {"name": "A1", "description": "account 1"},
+ {"name": "A2", "description": "account 2"},
+ ],
+ )
+
+ :param table_name: Name of the table
+ :param \*columns: collection of :class:`~sqlalchemy.schema.Column`
+ objects within
+ the table, as well as optional :class:`~sqlalchemy.schema.Constraint`
+ objects
+ and :class:`~.sqlalchemy.schema.Index` objects.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param \**kw: Other keyword arguments are passed to the underlying
+ :class:`sqlalchemy.schema.Table` object created for the command.
+
+ :return: the :class:`~sqlalchemy.schema.Table` object corresponding
+ to the parameters given.
+
+ """ # noqa: E501
+ ...
+
+ def create_table_comment(
+ self,
+ table_name: str,
+ comment: Optional[str],
+ *,
+ existing_comment: Optional[str] = None,
+ schema: Optional[str] = None,
+ ) -> None:
+ """Emit a COMMENT ON operation to set the comment for a table.
+
+ :param table_name: string name of the target table.
+ :param comment: string value of the comment being registered against
+ the specified table.
+ :param existing_comment: String value of a comment
+ already registered on the specified table, used within autogenerate
+ so that the operation is reversible, but not required for direct
+ use.
+
+ .. seealso::
+
+ :meth:`.Operations.drop_table_comment`
+
+ :paramref:`.Operations.alter_column.comment`
+
+ """ # noqa: E501
+ ...
+
+ def create_unique_constraint(
+ self,
+ constraint_name: Optional[str],
+ table_name: str,
+ columns: Sequence[str],
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> Any:
+ """Issue a "create unique constraint" instruction using the
+ current migration context.
+
+ e.g.::
+
+ from alembic import op
+ op.create_unique_constraint("uq_user_name", "user", ["name"])
+
+ This internally generates a :class:`~sqlalchemy.schema.Table` object
+ containing the necessary columns, then generates a new
+ :class:`~sqlalchemy.schema.UniqueConstraint`
+ object which it then associates with the
+ :class:`~sqlalchemy.schema.Table`.
+ Any event listeners associated with this action will be fired
+ off normally. The :class:`~sqlalchemy.schema.AddConstraint`
+ construct is ultimately used to generate the ALTER statement.
+
+ :param name: Name of the unique constraint. The name is necessary
+ so that an ALTER statement can be emitted. For setups that
+ use an automated naming scheme such as that described at
+ :ref:`sqla:constraint_naming_conventions`,
+ ``name`` here can be ``None``, as the event listener will
+ apply the name to the constraint object when it is associated
+ with the table.
+ :param table_name: String name of the source table.
+ :param columns: a list of string column names in the
+ source table.
+ :param deferrable: optional bool. If set, emit DEFERRABLE or
+ NOT DEFERRABLE when issuing DDL for this constraint.
+ :param initially: optional string. If set, emit INITIALLY
+ when issuing DDL for this constraint.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ def drop_column(
+ self,
+ table_name: str,
+ column_name: str,
+ *,
+ schema: Optional[str] = None,
+ **kw: Any,
+ ) -> None:
+ """Issue a "drop column" instruction using the current
+ migration context.
+
+ e.g.::
+
+ drop_column("organization", "account_id")
+
+ :param table_name: name of table
+ :param column_name: name of column
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+ :param mssql_drop_check: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop the CHECK constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from sys.check_constraints,
+ then exec's a separate DROP CONSTRAINT for that constraint.
+ :param mssql_drop_default: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop the DEFAULT constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from sys.default_constraints,
+ then exec's a separate DROP CONSTRAINT for that default.
+ :param mssql_drop_foreign_key: Optional boolean. When ``True``, on
+ Microsoft SQL Server only, first
+ drop a single FOREIGN KEY constraint on the column using a
+ SQL-script-compatible
+ block that selects into a @variable from
+ sys.foreign_keys/sys.foreign_key_columns,
+ then exec's a separate DROP CONSTRAINT for that default. Only
+ works if the column has exactly one FK constraint which refers to
+ it, at the moment.
+
+ """ # noqa: E501
+ ...
+
+ def drop_constraint(
+ self,
+ constraint_name: str,
+ table_name: str,
+ type_: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+ ) -> None:
+ r"""Drop a constraint of the given name, typically via DROP CONSTRAINT.
+
+ :param constraint_name: name of the constraint.
+ :param table_name: table name.
+ :param type\_: optional, required on MySQL. can be
+ 'foreignkey', 'primary', 'unique', or 'check'.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ """ # noqa: E501
+ ...
+
+ def drop_index(
+ self,
+ index_name: str,
+ table_name: Optional[str] = None,
+ *,
+ schema: Optional[str] = None,
+ if_exists: Optional[bool] = None,
+ **kw: Any,
+ ) -> None:
+ r"""Issue a "drop index" instruction using the current
+ migration context.
+
+ e.g.::
+
+ drop_index("accounts")
+
+ :param index_name: name of the index.
+ :param table_name: name of the owning table. Some
+ backends such as Microsoft SQL Server require this.
+ :param schema: Optional schema name to operate within. To control
+ quoting of the schema outside of the default behavior, use
+ the SQLAlchemy construct
+ :class:`~sqlalchemy.sql.elements.quoted_name`.
+
+ :param if_exists: If True, adds IF EXISTS operator when
+ dropping the index.
+
+ .. versionadded:: 1.12.0
+
+ :param \**kw: Additional keyword arguments not mentioned above are
+ dialect specific, and passed in the form
+ ``