diff --git a/README.md b/README.md index 226a5c2..4bfd422 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ chmod +x /home/tms/scripts/tms.sh 说明: - 脚本默认 `APP_HOME=/home/tms`,并通过 `--spring.config.additional-location=optional:file:/config/` 从 `/home/tms/config/` 读取外部配置。 - 默认要求 `/home/tms/config/application.yml` 存在。 -- 默认 profile 为 `prod`;可通过 `SPRING_PROFILES_ACTIVE=dev /home/tms/scripts/tms.sh start` 覆盖。 +- 脚本不再强制指定 profile;默认按 Spring 配置文件自身规则解析,可通过 `SPRING_PROFILES_ACTIVE=dev /home/tms/scripts/tms.sh start` 显式覆盖。 - 可通过 `JAR_PATH=/home/tms/tms-framework.jar` 覆盖 jar 路径。 - 如果部署路径不是 `/home/tms`,使用 `APP_HOME=/your/path /your/path/scripts/tms.sh start`。 - 标准 CISD 初始化辅助脚本位于 `scripts/standard-init/`,部署时需复制到 `/home/tms/bin/`。 @@ -75,9 +75,19 @@ Open: - Reset create task API: `POST /api/v1/init/reset/tasks` - Reset execute API: `POST /api/v1/init/reset/tasks/{taskId}/execute` - Auth login API: `POST /api/v1/auth/login` +- Compat password login API: `POST /api/v1/auth/password-login` +- Compat UKey login API: `POST /api/v1/auth/ukey-login` +- Compat UKey random API: `GET /api/v1/auth/ukey-login/randoms` +- UKey binding issue-sign API: `POST /api/v1/auth/roles/{roleCode}/ukeys/issue-sign` +- UKey binding API: `POST /api/v1/auth/roles/{roleCode}/ukeys/bind` - Device info API: `GET /api/v1/device/info` - Device profile API: `GET /api/v1/device/profile` - Device runtime status API: `GET /api/v1/device/runtime-status` +- Upgrade package upload API: `POST /api/v1/upgrade-packages` +- Upgrade preview API: `POST /api/v1/upgrades/previews` +- Upgrade create task API: `POST /api/v1/upgrades` +- Upgrade execute API: `POST /api/v1/upgrades/{taskId}/execute` +- Upgrade list API: `GET /api/v1/upgrades` - PCIe crypto count API: `GET /api/v1/device/crypto/device-count` - PCIe crypto HMAC API: `POST /api/v1/device/crypto/hmac` @@ -139,6 +149,70 @@ Open: - `tms.init.executor.rabbitmq-ctl-command` 指向可执行的 `rabbitmqctl` - 运行时 Swagger 文档以 `http://:8080/swagger-ui.html` 和 `http://:8080/v3/api-docs/internal-api` 为准 +## Offline Upgrade + +最小调用顺序: +- `POST /api/v1/upgrade-packages` +- `POST /api/v1/upgrades/previews` +- `POST /api/v1/upgrades` +- `POST /api/v1/upgrades/{taskId}/execute` +- `GET /api/v1/upgrades` +- `GET /api/v1/upgrades/{taskId}` +- `GET /api/v1/upgrades/{taskId}/log` + +当前支持范围: +- 仅支持离线升级 +- 当前支持升级对象: + - `TMS` + - `STANDARD_APP` + - `DIRECT_APP` +- 当前不支持: + - 在线升级 + - `CVE` / 操作系统补丁升级 + - 密码卡固件升级 + +当前流程: +- 先上传离线升级包,复用现有文件上传能力,得到 `fileId` +- 后端基于 `fileId` 解析升级包并做预检 +- 预检结果返回升级对象、当前版本、目标版本和版本变化提示 +- 用户确认后创建升级任务 +- 调用执行接口后,任务异步进入后台执行 +- 前端通过列表、详情和日志接口轮询任务状态 + +当前升级包要求: +- 外层为 zip 包 +- 至少包含: + - `manifest.json` + - `signature.sig` + - `payload.zip` +- `signature.sig` 需要能被设备密码卡使用“固件完整性密钥”完成验签 +- 服务端需要配置 `tms.upgrade.signature-public-key-base64`(Base64 编码的公钥 blob) +- `manifest.json` 当前要求包含: + - `packageId` + - `targetComponent` + - `targetVersion` + - `minCompatibleVersion` + - `description` + - `orchestrator` + - `fileChecksums.payloadZip` + +当前执行规则: +- 同一时刻只允许一个升级任务处于 `RUNNING` +- 不允许目标版本低于当前版本 +- 不满足最小兼容版本时拒绝升级 +- 升级成功后会更新 `tms_device_software_version` +- 升级执行日志统一写入 `tms.upgrade.log-dir` + +当前配置项: +- `tms.upgrade.staging-root-dir` +- `tms.upgrade.log-dir` +- `tms.upgrade.signature-public-key-base64` + +升级包规范见: +- [2026-03-19-upgrade-package-spec.md](/Users/waner/Work/CISD/文档/tms-framework/docs/plans/2026-03-19-upgrade-package-spec.md) +- [标准收发器升级包最小样例](/Users/waner/Work/CISD/文档/tms-framework/docs/examples/upgrade-package/standard-app/README.md) +- [离线升级包预检联调清单与示例](/Users/waner/Work/CISD/文档/tms-framework/docs/plans/2026-03-19-upgrade-preview-integration-guide.md) + ## Architecture (Monolith + Modular) - One deployable Spring Boot application. @@ -189,6 +263,13 @@ src/main/java/com/cisd/tms │ └── entity ├── init ├── auth + +## Auth Compat + +当前仓库只包含后端认证接口,不包含登录页前端工程。 + +兼容旧管理端的登录调用说明见: +- [2026-03-23-auth-compat-integration-guide.md](/Users/waner/Work/CISD/文档/tms-framework/docs/plans/2026-03-23-auth-compat-integration-guide.md) ├── device ├── upgrade ├── cert @@ -238,8 +319,8 @@ tms: crypto-card: enabled: ${TMS_CRYPTO_CARD_ENABLED:false} mode: ${TMS_CRYPTO_CARD_MODE:MOCK} # MOCK / JNA - vendor-lib-path: ${TMS_CRYPTO_VENDOR_LIB_PATH:} - vendor-lib-name: ${TMS_CRYPTO_VENDOR_LIB_NAME:swsdsdf} + vendor-lib-path: ${TMS_CRYPTO_VENDOR_LIB_PATH:/home/tms/libs/libsdf_ukey_syd1408-x64.so.1.0.0} + vendor-lib-name: ${TMS_CRYPTO_VENDOR_LIB_NAME:sdf_ukey_syd1408} ``` - Internal PCIe debug APIs (strong-typed service): @@ -280,8 +361,8 @@ tms: crypto-card: enabled: ${TMS_CRYPTO_CARD_ENABLED:false} mode: ${TMS_CRYPTO_CARD_MODE:JNA} # MOCK / JNA - vendor-lib-path: ${TMS_CRYPTO_VENDOR_LIB_PATH:} - vendor-lib-name: ${TMS_CRYPTO_VENDOR_LIB_NAME:swsdsdf} + vendor-lib-path: ${TMS_CRYPTO_VENDOR_LIB_PATH:/home/tms/libs/libsdf_ukey_syd1408-x64.so.1.0.0} + vendor-lib-name: ${TMS_CRYPTO_VENDOR_LIB_NAME:sdf_ukey_syd1408} ``` - JNA native mapping: `com.cisd.tms.integration.crypto.pcie.jna.PcieNativeLibrary` diff --git a/config/application.yml.example b/config/application.yml.example index 4be643a..1cf2e48 100644 --- a/config/application.yml.example +++ b/config/application.yml.example @@ -6,8 +6,43 @@ server: port: 8080 tms: + web: + cors: + enabled: true + allowed-origin-patterns: + - http://localhost:* + - http://127.0.0.1:* + allowed-methods: + - GET + - POST + - PUT + - DELETE + - OPTIONS + allowed-headers: + - Authorization + - Content-Type + - X-Requested-With + - X-Internal-Token + - X-App-Id + - X-Timestamp + - X-Nonce + - X-Signature + - X-Trace-Id + exposed-headers: + - X-Trace-Id + allow-credentials: true + max-age-seconds: 3600 + upgrade: + staging-root-dir: /home/tmp/tms-upgrade-staging + log-dir: /home/tms/tmp/tms-upgrade-logs + signature-public-key-base64: change-me-base64-public-key-blob security: internal-token: change-me-internal-token + internal-auth: + enabled: true + debug-role-code: KEY_ADMIN + debug-auth-level: FULL + debug-session-token: DEBUG-BYPASS openapi: timestamp-skew-seconds: 300 clients: diff --git a/docs/deployment/tms-deployment.md b/docs/deployment/tms-deployment.md index 79c9682..cf4840f 100644 --- a/docs/deployment/tms-deployment.md +++ b/docs/deployment/tms-deployment.md @@ -137,7 +137,7 @@ mvn -q -DskipTests package ### 6.2 创建运行目录 ```bash -sudo mkdir -p /home/tms/{bin,config,scripts,logs,run,tmp,uploads} +sudo mkdir -p /home/tms/{bin,config,scripts,logs,run,tmp,uploads,libs} sudo chown -R "$(whoami)":"$(whoami)" /home/tms ``` @@ -165,6 +165,7 @@ ls -l /home/tms/config/application.yml TMS 从以下位置加载外部配置: - `/home/tms/config/application.yml` +- 显式设置 `SPRING_PROFILES_ACTIVE` 时,额外加载 `/home/tms/config/application-\${SPRING_PROFILES_ACTIVE}.yml` 运行控制脚本: - [tms.sh](/Users/waner/Work/CISD/文档/tms-framework/scripts/tms.sh) diff --git a/docs/examples/upgrade-package/standard-app/README.md b/docs/examples/upgrade-package/standard-app/README.md new file mode 100644 index 0000000..1b95d54 --- /dev/null +++ b/docs/examples/upgrade-package/standard-app/README.md @@ -0,0 +1,49 @@ +# 标准收发器离线升级包最小样例 + +本目录提供 `STANDARD_APP` 离线升级包的最小示例骨架,用于对齐打包方、后端和联调流程。 + +目录结构: + +```text +standard-app/ +├── manifest.json +└── payload/ + └── scripts/ + └── orchestrator.sh +``` + +说明: + +- 本目录中的 `manifest.json` 使用了占位符 `__PAYLOAD_SHA256__` +- 真正打包前,需要先对 `payload.zip` 计算 `SHA-256`,再替换该字段 +- `signature.sig` 不放在仓库示例里,必须由打包方使用“固件完整性密钥”私钥在打包阶段生成 + +推荐打包步骤: + +1. 进入当前示例目录 +2. 将 `payload/` 压缩为 `payload.zip` +3. 计算 `payload.zip` 的 `SHA-256` +4. 用实际摘要替换 `manifest.json` 中的 `__PAYLOAD_SHA256__` +5. 对 `manifest.json` 原始字节生成 `signature.sig` +6. 将 `manifest.json`、`signature.sig`、`payload.zip` 压缩成最终 `upgrade-package.zip` + +参考命令: + +```bash +cd docs/examples/upgrade-package/standard-app + +zip -rq payload.zip payload +PAYLOAD_SHA256="$(shasum -a 256 payload.zip | awk '{print $1}')" +sed "s/__PAYLOAD_SHA256__/${PAYLOAD_SHA256}/g" manifest.json > manifest.generated.json + +# 这里改为公司正式签名工具,生成 manifest.generated.json 对应的 signature.sig +# sign-manifest manifest.generated.json > signature.sig + +zip -j upgrade-package.zip manifest.generated.json signature.sig payload.zip +``` + +联调时建议: + +- 先用这个目录骨架做一个 `STANDARD_APP` 预检样包 +- `orchestrator.sh` 可以先只输出日志并返回 `0` +- 等预检、建任务、执行链路跑通后,再替换成真实升级脚本 diff --git a/docs/examples/upgrade-package/standard-app/manifest.json b/docs/examples/upgrade-package/standard-app/manifest.json new file mode 100644 index 0000000..c9c461e --- /dev/null +++ b/docs/examples/upgrade-package/standard-app/manifest.json @@ -0,0 +1,13 @@ +{ + "packageFormatVersion": 1, + "packageId": "PKG-20260319-STANDARD-001", + "targetComponent": "STANDARD_APP", + "targetVersion": "V1.3.2", + "minCompatibleVersion": "V1.3.0", + "description": "标准收发器企业版离线升级包最小样例", + "createdAt": "2026-03-19T16:30:00+08:00", + "orchestrator": "scripts/orchestrator.sh", + "fileChecksums": { + "payloadZip": "__PAYLOAD_SHA256__" + } +} diff --git a/docs/examples/upgrade-package/standard-app/payload/scripts/orchestrator.sh b/docs/examples/upgrade-package/standard-app/payload/scripts/orchestrator.sh new file mode 100644 index 0000000..77ef40d --- /dev/null +++ b/docs/examples/upgrade-package/standard-app/payload/scripts/orchestrator.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TASK_ID="${1:-UNKNOWN}" + +echo "[upgrade] task=${TASK_ID} phase=backup message=start backup placeholder" +echo "[upgrade] task=${TASK_ID} phase=stop-service message=stop service placeholder" +echo "[upgrade] task=${TASK_ID} phase=deploy message=deploy placeholder" +echo "[upgrade] task=${TASK_ID} phase=health-check message=health check placeholder" +echo "[upgrade] task=${TASK_ID} phase=done message=upgrade completed" diff --git a/docs/sql/tms_schema_full.sql b/docs/sql/tms_schema_full.sql index 019981b..1119634 100644 --- a/docs/sql/tms_schema_full.sql +++ b/docs/sql/tms_schema_full.sql @@ -1,70 +1,314 @@ -- ----------------------------------------------------------------------------- --- TMS 基础库结构(TiDB/MySQL) --- 适用范围:当前已实现的初始化任务、步骤、设备节点、用户表 +-- TMS 最终库结构初始化脚本 +-- 适用范围:新库初始化 +-- 数据库:TiDB / MySQL 8+ +-- +-- 说明: +-- 1. 本文件按当前项目最终表结构整理,适合“空库初始化”。 +-- 2. 不再按 V1-V11 逐步执行 ALTER / RENAME,避免初始化时出现兼容性问题。 +-- 3. 若用于已有库升级,请继续使用 Flyway migration,而不是本文件。 -- ----------------------------------------------------------------------------- --- 1) 建库(如已存在则跳过) CREATE DATABASE IF NOT EXISTS tms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE tms; --- 2) 初始化任务主表 +-- ----------------------------------------------------------------------------- +-- 初始化任务 +-- ----------------------------------------------------------------------------- + CREATE TABLE IF NOT EXISTS tms_init_task ( - id BIGINT PRIMARY KEY COMMENT '主键ID', - task_id VARCHAR(64) NOT NULL COMMENT '任务业务ID', - version VARCHAR(32) NOT NULL COMMENT 'CIPS版本', - status VARCHAR(32) NOT NULL COMMENT '任务状态', - product_type VARCHAR(16) NULL COMMENT '产品版本: enterprise/interbank/lite_direct', - org_code VARCHAR(32) NULL COMMENT '机构号', - mq_type VARCHAR(32) NULL COMMENT '消息队列类型', - deploy_mode VARCHAR(16) NULL COMMENT '部署模式: single/double/four', - init_plan_json LONGTEXT NULL COMMENT '初始化计划快照(JSON)', - create_time DATETIME(3) NOT NULL COMMENT '创建时间', - update_time DATETIME(3) NOT NULL COMMENT '更新时间', + id BIGINT PRIMARY KEY, + task_id VARCHAR(64) NOT NULL, + task_type VARCHAR(32) NOT NULL DEFAULT 'INIT' COMMENT '任务类型:INIT/RESET', + version VARCHAR(32) NOT NULL, + status VARCHAR(32) NOT NULL, + product_type VARCHAR(16) NULL, + org_code VARCHAR(32) NULL, + mq_type VARCHAR(32) NULL, + deploy_mode VARCHAR(16) NULL, + init_plan_json LONGTEXT NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, UNIQUE KEY uk_tms_init_task_task_id (task_id) -) COMMENT='初始化任务表'; +); --- 3) 初始化任务步骤表 CREATE TABLE IF NOT EXISTS tms_init_task_step ( - id BIGINT PRIMARY KEY COMMENT '主键ID', - task_id VARCHAR(64) NOT NULL COMMENT '任务业务ID', - step_no INT NOT NULL COMMENT '步骤序号', - step_code VARCHAR(64) NOT NULL COMMENT '步骤编码', - status VARCHAR(32) NOT NULL COMMENT '步骤状态', - message VARCHAR(512) NULL COMMENT '步骤信息', - command_line VARCHAR(2048) NULL COMMENT '实际执行命令', - log_path VARCHAR(512) NULL COMMENT '日志文件路径', - exit_code INT NULL COMMENT '命令退出码', - create_time DATETIME(3) NOT NULL COMMENT '创建时间', - update_time DATETIME(3) NOT NULL COMMENT '更新时间', + id BIGINT PRIMARY KEY, + task_id VARCHAR(64) NOT NULL, + step_no INT NOT NULL, + step_code VARCHAR(64) NOT NULL, + status VARCHAR(32) NOT NULL, + message VARCHAR(512) NULL, + command_line VARCHAR(2048) NULL, + log_path VARCHAR(512) NULL, + exit_code INT NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, UNIQUE KEY uk_tms_init_task_step_task_no (task_id, step_no), KEY idx_tms_init_task_step_task_id (task_id) -) COMMENT='初始化任务步骤表'; +); + +-- ----------------------------------------------------------------------------- +-- 认证与设备基础表 +-- ----------------------------------------------------------------------------- --- 4) 系统用户表(简化) CREATE TABLE IF NOT EXISTS tms_auth_user ( - id BIGINT PRIMARY KEY COMMENT '主键ID', - username VARCHAR(64) NOT NULL COMMENT '用户名', - display_name VARCHAR(128) NOT NULL COMMENT '显示名', - role VARCHAR(64) NOT NULL COMMENT '角色', - create_time DATETIME(3) NOT NULL COMMENT '创建时间', - update_time DATETIME(3) NOT NULL COMMENT '更新时间', + id BIGINT PRIMARY KEY, + username VARCHAR(64) NOT NULL, + display_name VARCHAR(128) NOT NULL, + role VARCHAR(64) NOT NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, UNIQUE KEY uk_tms_auth_user_username (username) -) COMMENT='系统用户表'; +); --- 5) 设备节点表 CREATE TABLE IF NOT EXISTS tms_device_node ( - id BIGINT PRIMARY KEY COMMENT '主键ID', - node_id VARCHAR(64) NOT NULL COMMENT '节点业务ID', - host VARCHAR(128) NOT NULL COMMENT '节点主机地址', - status VARCHAR(32) NOT NULL COMMENT '节点状态', - create_time DATETIME(3) NOT NULL COMMENT '创建时间', - update_time DATETIME(3) NOT NULL COMMENT '更新时间', + id BIGINT PRIMARY KEY, + node_id VARCHAR(64) NOT NULL, + host VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, UNIQUE KEY uk_tms_device_node_node_id (node_id) -) COMMENT='设备节点表'; +); --- 6) 可选初始化数据(按需启用) --- INSERT INTO tms_auth_user (id, username, display_name, role, create_time, update_time) --- VALUES (10001, 'admin', '系统管理员', 'ADMIN', NOW(3), NOW(3)); +CREATE TABLE IF NOT EXISTS tms_file_record ( + id BIGINT PRIMARY KEY, + file_id VARCHAR(64) NOT NULL, + original_filename VARCHAR(255) NOT NULL, + storage_path VARCHAR(1024) NOT NULL, + status VARCHAR(32) NOT NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_tms_file_record_file_id (file_id) +); + +-- ----------------------------------------------------------------------------- +-- 设备软件版本 +-- ----------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS tms_device_software_version ( + id BIGINT PRIMARY KEY, + component_code VARCHAR(64) NOT NULL, + component_name VARCHAR(128) NOT NULL, + current_version VARCHAR(128) NOT NULL, + source_type VARCHAR(32) NOT NULL, + detected_at DATETIME NULL, + remarks VARCHAR(255) NULL, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uk_tms_device_software_version_component_code (component_code) +); + +INSERT INTO tms_device_software_version +(id, component_code, component_name, current_version, source_type, detected_at, remarks) +VALUES + (1001, 'RABBITMQ', '消息队列', 'RabbitMQ V3.8.21', 'MANUAL', NOW(), '出厂预置'), + (1002, 'TLQ', '消息队列', 'tonglink/Q V8.1.11', 'MANUAL', NOW(), '出厂预置'), + (1003, 'TIDB', '数据库', 'TiDB V8.1.0', 'MANUAL', NOW(), '出厂预置'), + (1004, 'NGINX', 'Nginx', 'Nginx V1.20', 'MANUAL', NOW(), '出厂预置'), + (1005, 'APP', '标准收发器企业版', '标准收发器企业版 V1.3.2', 'MANUAL', NOW(), '出厂预置'), + (1006, 'TMS', 'CISD终端管理服务', 'CISD终端管理服务 V1.0.0', 'MANUAL', NOW(), '出厂预置') +ON DUPLICATE KEY UPDATE + component_name = VALUES(component_name), + current_version = VALUES(current_version), + source_type = VALUES(source_type), + detected_at = VALUES(detected_at), + remarks = VALUES(remarks), + update_time = CURRENT_TIMESTAMP; + +-- ----------------------------------------------------------------------------- +-- 升级任务 +-- ----------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS t_upgrade_task ( + id BIGINT PRIMARY KEY, + task_id VARCHAR(64) NOT NULL, + upgrade_mode VARCHAR(16) NOT NULL, + target_component VARCHAR(32) NOT NULL, + package_file_id VARCHAR(64) NOT NULL, + package_name VARCHAR(255) NOT NULL, + package_hash VARCHAR(128) NOT NULL, + current_version VARCHAR(64) NOT NULL, + target_version VARCHAR(64) NOT NULL, + status VARCHAR(32) NOT NULL, + verify_passed TINYINT(1) NOT NULL DEFAULT 0, + verify_message VARCHAR(1024) NULL, + manifest_json TEXT NULL, + detail_log_path VARCHAR(1024) NULL, + error_message VARCHAR(1024) NULL, + operator VARCHAR(64) NULL, + started_at DATETIME(3) NULL, + finished_at DATETIME(3) NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_t_upgrade_task_task_id (task_id), + KEY idx_t_upgrade_task_status (status), + KEY idx_t_upgrade_task_component (target_component), + KEY idx_t_upgrade_task_create_time (create_time) +); + +-- ----------------------------------------------------------------------------- +-- 新认证体系 +-- ----------------------------------------------------------------------------- + +CREATE TABLE IF NOT EXISTS tms_role_account ( + id BIGINT PRIMARY KEY, + role_code VARCHAR(64) NOT NULL, + display_name VARCHAR(128) NOT NULL, + required_ukey_count INT NOT NULL, + password_hash VARCHAR(256) NOT NULL, + password_salt VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL, + need_change_password TINYINT(1) NOT NULL DEFAULT 1, + failed_count INT NOT NULL DEFAULT 0, + locked_until DATETIME(3) NULL, + last_login_at DATETIME(3) NULL, + last_active_at DATETIME(3) NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_tms_role_account_role_code (role_code) +); + +CREATE TABLE IF NOT EXISTS tms_role_ukey_binding ( + id BIGINT PRIMARY KEY, + role_code VARCHAR(64) NOT NULL, + slot_no INT NOT NULL, + ukey_serial VARCHAR(128) NOT NULL, + ukey_pubkey TEXT NOT NULL, + issuer_sign TEXT NULL, + status VARCHAR(32) NOT NULL, + bound_at DATETIME(3) NOT NULL, + unbound_at DATETIME(3) NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_tms_role_ukey_binding_role_slot (role_code, slot_no), + KEY idx_tms_role_ukey_binding_role_status (role_code, status) +); + +CREATE TABLE IF NOT EXISTS tms_auth_session ( + id BIGINT PRIMARY KEY, + session_token VARCHAR(128) NOT NULL, + role_code VARCHAR(64) NOT NULL, + auth_level VARCHAR(32) NOT NULL, + issued_at DATETIME(3) NOT NULL, + last_active_at DATETIME(3) NOT NULL, + expires_at DATETIME(3) NOT NULL, + logout_at DATETIME(3) NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_tms_auth_session_token (session_token), + KEY idx_tms_auth_session_role_code (role_code) +); + +CREATE TABLE IF NOT EXISTS tms_auth_challenge ( + id BIGINT PRIMARY KEY, + challenge_id VARCHAR(128) NOT NULL, + role_code VARCHAR(64) NOT NULL, + purpose VARCHAR(32) NOT NULL, + nonce VARCHAR(256) NOT NULL, + expires_at DATETIME(3) NOT NULL, + used TINYINT(1) NOT NULL DEFAULT 0, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_tms_auth_challenge_challenge_id (challenge_id) +); + +CREATE TABLE IF NOT EXISTS tms_auth_audit_log ( + id BIGINT PRIMARY KEY, + event_type VARCHAR(64) NOT NULL, + role_code VARCHAR(64) NULL, + operator_role_code VARCHAR(64) NULL, + result VARCHAR(32) NOT NULL, + detail_json JSON NULL, + remote_ip VARCHAR(64) NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + KEY idx_tms_auth_audit_log_event_type (event_type), + KEY idx_tms_auth_audit_log_role_code (role_code) +); + +INSERT IGNORE INTO tms_role_account ( + id, + role_code, + display_name, + required_ukey_count, + password_hash, + password_salt, + status, + need_change_password, + failed_count, + locked_until, + last_login_at, + last_active_at, + create_time, + update_time +) +VALUES + ( + 11001, + 'SUPER_ADMIN', + '超级管理员', + 3, + 'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=', + 'init-super-admin-salt-20260325', + 'UNENABLED', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ), + ( + 11002, + 'KEY_ADMIN', + '密钥管理员', + 2, + '/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=', + 'init-key-admin-salt-20260325', + 'ACTIVE', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ), + ( + 11003, + 'AUDIT_ADMIN', + '审计管理员', + 1, + 'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=', + 'init-audit-admin-salt-20260325', + 'UNENABLED', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ), + ( + 11004, + 'OPS_ADMIN', + '运维管理员', + 1, + '2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=', + 'init-ops-admin-salt-20260325', + 'UNENABLED', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ); diff --git a/docs/sql/tms_schema_init_steps.md b/docs/sql/tms_schema_init_steps.md new file mode 100644 index 0000000..6e2945e --- /dev/null +++ b/docs/sql/tms_schema_init_steps.md @@ -0,0 +1,79 @@ +# TMS 初始化 SQL 执行说明 + +当前推荐使用 [tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/docs/sql/tms_schema_full.sql)。 + +这份脚本已经整理成“最终库结构初始化版”,适合空库直接初始化,不再按 migration 逐步执行 `ALTER / RENAME`。 + +如果你在 TiDB 客户端里一次性执行整份文件,出现下面这类报错: + +```text +SQL Error [8130] [HY000]: client has multi-statement capability disabled +``` + +这不是 SQL 内容本身错误,而是“客户端把整份脚本作为一次多语句请求发送”,而当前 TiDB 连接未开启 multi-statement。 + +## 推荐做法 + +### 方式一:用脚本执行模式运行最终初始化脚本 + +优先用客户端的 `Run Script` / `Execute Script` 模式执行: + +- [tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/docs/sql/tms_schema_full.sql) + +### 方式二:用“脚本执行”而不是“单次执行” + +如果你的客户端支持 `Run Script` / `Execute Script`,优先用脚本模式。 +脚本模式通常会把每条语句分开发送,而不是整份文件一次发给数据库。 + +### 方式三:使用命令行客户端导入 + +例如: + +```bash +mysql -h -P -u -p tms < /path/to/tms_schema_full.sql +``` + +或者先登录后: + +```sql +source /path/to/tms_schema_full.sql; +``` + +### 方式四:实在只能单条执行时 + +先执行: + +```sql +CREATE DATABASE IF NOT EXISTS tms + DEFAULT CHARACTER SET utf8mb4 + COLLATE utf8mb4_general_ci; + +USE tms; +``` + +然后把 [tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/docs/sql/tms_schema_full.sql) 中的语句按分号逐条执行。 + +## 不推荐但可选 + +你也可以按报错提示开启: + +```sql +SET GLOBAL tidb_multi_statement_mode='ON'; +``` + +但这会影响实例级行为,有安全风险,不建议为了初始化脚本临时这么做,除非你明确知道数据库侧的风险边界并且由 DBA 允许。 + +## 当前默认账号种子 + +脚本已经包含 4 个固定角色账号: + +- `SUPER_ADMIN` +- `KEY_ADMIN` +- `AUDIT_ADMIN` +- `OPS_ADMIN` + +默认口令均为 `12345678`,其中: + +- `KEY_ADMIN` 默认 `ACTIVE` +- 其他角色默认 `UNENABLED` +- 所有角色 `need_change_password = 1` diff --git a/openapi.yml b/openapi.yml new file mode 100644 index 0000000..8ad567f --- /dev/null +++ b/openapi.yml @@ -0,0 +1,2427 @@ +openapi: 3.0.3 +info: + title: CISD TMS Crypto Card Internal API (Full) + version: 1.1.0 + description: | + 覆盖 CryptoCardController 与扩展密码卡接口的全部能力。 +servers: + - url: http://localhost:8080 + description: Local + +tags: + - name: CryptoAll + description: 密码卡全量接口 + +security: + - InternalTokenAuth: [] + +paths: + /api/v1/device/crypto/agreement/data-key/ecc: + post: + tags: [CryptoAll] + summary: generateAgreementDataAndKeyEcc + operationId: generateAgreementDataAndKeyEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateAgreementDataAndKeyEccInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/agreement/session-key/ecc: + post: + tags: [CryptoAll] + summary: generateSessionKeyEcc + operationId: generateSessionKeyEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateSessionKeyEccInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/decrypt/kek: + post: + tags: [CryptoAll] + summary: decryptKek + operationId: decryptKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricKekInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/decrypt/plain: + post: + tags: [CryptoAll] + summary: decryptPlain + operationId: decryptPlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/device-conf: + get: + tags: [CryptoAll] + summary: getDeviceConf + operationId: getDeviceConf + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/device-count: + get: + tags: [CryptoAll] + summary: getDeviceCount + operationId: getDeviceCount + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/device-info: + get: + tags: [CryptoAll] + summary: getDeviceInfo + operationId: getDeviceInfo + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/digest: + post: + tags: [CryptoAll] + summary: digest + operationId: digest + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoDigestInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/encrypt/kek: + post: + tags: [CryptoAll] + summary: encryptKek + operationId: encryptKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricKekInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/encrypt/plain: + post: + tags: [CryptoAll] + summary: encryptPlain + operationId: encryptPlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoSymmetricInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/envelope/exchange/ecc: + post: + tags: [CryptoAll] + summary: exchangeEnvelopeEcc + operationId: exchangeEnvelopeEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoExchangeEnvelopeEccInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/envelope/exchange/rsa: + post: + tags: [CryptoAll] + summary: exchangeEnvelopeRsa + operationId: exchangeEnvelopeRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoExchangeEnvelopeRsaInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/all-key/backup: + post: + tags: [CryptoAll] + summary: backupAllKey + operationId: backupAllKey + parameters: + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/all-key/restore: + post: + tags: [CryptoAll] + summary: restoreAllKey + operationId: restoreAllKey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/device/factory-reset: + post: + tags: [CryptoAll] + summary: factoryReset + operationId: factoryReset + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/device/init-identify: + post: + tags: [CryptoAll] + summary: initIdentify + operationId: initIdentify + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64PairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/device/status: + get: + tags: [CryptoAll] + summary: getDeviceStatus + operationId: getDeviceStatus + parameters: + - name: statusBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/decrypt: + post: + tags: [CryptoAll] + summary: eccExternalDecrypt + operationId: eccExternalDecrypt + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalDecryptRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/encrypt: + post: + tags: [CryptoAll] + summary: eccExternalEncrypt + operationId: eccExternalEncrypt + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalEncryptRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/sign: + post: + tags: [CryptoAll] + summary: eccExternalSign + operationId: eccExternalSign + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalSignRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/external/verify: + post: + tags: [CryptoAll] + summary: eccExternalVerify + operationId: eccExternalVerify + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccExternalVerifyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/internal/sign: + post: + tags: [CryptoAll] + summary: eccInternalSign + operationId: eccInternalSign + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccInternalSignRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ecc/internal/verify: + post: + tags: [CryptoAll] + summary: eccInternalVerify + operationId: eccInternalVerify + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EccInternalVerifyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/backup: + post: + tags: [CryptoAll] + summary: backupIk + operationId: backupIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/destroy: + post: + tags: [CryptoAll] + summary: destroyIk + operationId: destroyIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/generate: + post: + tags: [CryptoAll] + summary: generateIk + operationId: generateIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/recover: + post: + tags: [CryptoAll] + summary: recoverIk + operationId: recoverIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ik/status: + get: + tags: [CryptoAll] + summary: statusIk + operationId: statusIk + parameters: + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/backup: + post: + tags: [CryptoAll] + summary: backupKek + operationId: backupKek + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/component/export: + post: + tags: [CryptoAll] + summary: exportKekComponent + operationId: exportKekComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ExportKekComponentRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/component/import: + post: + tags: [CryptoAll] + summary: importKekComponent + operationId: importKekComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/KekComponentRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/delete: + post: + tags: [CryptoAll] + summary: deleteKek + operationId: deleteKek + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/import: + post: + tags: [CryptoAll] + summary: importKek + operationId: importKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKekRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/kek/recover: + post: + tags: [CryptoAll] + summary: recoverKek + operationId: recoverKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RecoverKekRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/key/update-backed: + post: + tags: [CryptoAll] + summary: updateBackedKey + operationId: updateBackedKey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateBackedKeyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/ecc/generate: + post: + tags: [CryptoAll] + summary: generateKeyPairEcc + operationId: generateKeyPairEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/ecc/import: + post: + tags: [CryptoAll] + summary: importKeyPairEcc + operationId: importKeyPairEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/ecc/import-encrypted: + post: + tags: [CryptoAll] + summary: importEncryptedKeyPairEcc + operationId: importEncryptedKeyPairEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportEncKeyPairEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/rsa/generate: + post: + tags: [CryptoAll] + summary: generateKeyPairRsa + operationId: generateKeyPairRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/rsa/import: + post: + tags: [CryptoAll] + summary: importKeyPairRsa + operationId: importKeyPairRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyPairRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/keypair/rsa/import-encrypted: + post: + tags: [CryptoAll] + summary: importEncryptedKeyPairRsa + operationId: importEncryptedKeyPairRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportEncKeyPairRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/backup: + post: + tags: [CryptoAll] + summary: backupLmk + operationId: backupLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/backup-ex: + post: + tags: [CryptoAll] + summary: backupLmkEx + operationId: backupLmkEx + parameters: + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/destroy: + post: + tags: [CryptoAll] + summary: destroyLmk + operationId: destroyLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/generate: + post: + tags: [CryptoAll] + summary: generateLmk + operationId: generateLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/load: + post: + tags: [CryptoAll] + summary: loadLmk + operationId: loadLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/recover: + post: + tags: [CryptoAll] + summary: recoverLmk + operationId: recoverLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/lmk/recover-ex: + post: + tags: [CryptoAll] + summary: recoverLmkEx + operationId: recoverLmkEx + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/private-key/access-right/get: + post: + tags: [CryptoAll] + summary: getPrivateKeyAccessRight + operationId: getPrivateKeyAccessRight + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PrivateKeyAccessRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/private-key/access-right/release: + post: + tags: [CryptoAll] + summary: releasePrivateKeyAccessRight + operationId: releasePrivateKeyAccessRight + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/private-key/password/change: + post: + tags: [CryptoAll] + summary: changePrivateKeyAccessPassword + operationId: changePrivateKeyAccessPassword + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePrivateKeyPasswordRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/ecc/enc/export: + get: + tags: [CryptoAll] + summary: exportEncPublicKeyEcc + operationId: exportEncPublicKeyEcc + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/ecc/sign/export: + get: + tags: [CryptoAll] + summary: exportSignPublicKeyEcc + operationId: exportSignPublicKeyEcc + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/rsa/enc/export: + get: + tags: [CryptoAll] + summary: exportEncPublicKeyRsa + operationId: exportEncPublicKeyRsa + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/public-key/rsa/sign/export: + get: + tags: [CryptoAll] + summary: exportSignPublicKeyRsa + operationId: exportSignPublicKeyRsa + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/external/private-op: + post: + tags: [CryptoAll] + summary: rsaExternalPrivateOp + operationId: rsaExternalPrivateOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaExternalPrivateKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/external/public-op: + post: + tags: [CryptoAll] + summary: rsaExternalPublicOp + operationId: rsaExternalPublicOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaExternalPublicKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/internal/private-op: + post: + tags: [CryptoAll] + summary: rsaInternalPrivateOp + operationId: rsaInternalPrivateOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaInternalKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/rsa/internal/public-op: + post: + tags: [CryptoAll] + summary: rsaInternalPublicOp + operationId: rsaInternalPublicOp + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RsaInternalKeyOpRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/ecc/epk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithEpkEcc + operationId: generateKeyWithEpkEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithEpkEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/ecc/ipk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithIpkEcc + operationId: generateKeyWithIpkEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithIpkEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/ecc/isk/import: + post: + tags: [CryptoAll] + summary: importKeyWithIskEcc + operationId: importKeyWithIskEcc + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyWithIskEccRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/kek/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithKek + operationId: generateKeyWithKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithKekRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/rsa/epk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithEpkRsa + operationId: generateKeyWithEpkRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithEpkRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/rsa/ipk/generate: + post: + tags: [CryptoAll] + summary: generateKeyWithIpkRsa + operationId: generateKeyWithIpkRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateKeyWithIpkRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/session-key/rsa/isk/import: + post: + tags: [CryptoAll] + summary: importKeyWithIskRsa + operationId: importKeyWithIskRsa + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ImportKeyWithIskRsaRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/certificate/generate: + post: + tags: [CryptoAll] + summary: generateCertificate + operationId: generateCertificate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/generate: + post: + tags: [CryptoAll] + summary: generateUkey + operationId: generateUkey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyGenerateRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/ik-component/read: + post: + tags: [CryptoAll] + summary: readUkeyIkComponent + operationId: readUkeyIkComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/KeyTypePinRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/lmk-component/read: + post: + tags: [CryptoAll] + summary: readLmkComponent + operationId: readLmkComponent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Base64DataRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/login: + post: + tags: [CryptoAll] + summary: loginUkey + operationId: loginUkey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyLoginRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/login-status: + get: + tags: [CryptoAll] + summary: loginStatus + operationId: loginStatus + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/logout-admin: + post: + tags: [CryptoAll] + summary: logoutAdmin + operationId: logoutAdmin + parameters: + - name: adminIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/pin/change: + post: + tags: [CryptoAll] + summary: changeUkeyPin + operationId: changeUkeyPin + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyPinChangeRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/ukey/unlock: + post: + tags: [CryptoAll] + summary: unlockUkey + operationId: unlockUkey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UkeyUnlockRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/backup: + post: + tags: [CryptoAll] + summary: backupUserKey + operationId: backupUserKey + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + - name: outBufferSize + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/delete: + post: + tags: [CryptoAll] + summary: deleteUserKey + operationId: deleteUserKey + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/ecc/status: + get: + tags: [CryptoAll] + summary: statusUserKeyEcc + operationId: statusUserKeyEcc + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/recover: + post: + tags: [CryptoAll] + summary: recoverUserKey + operationId: recoverUserKey + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RecoverUserKeyRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/ext/user-key/rsa/status: + get: + tags: [CryptoAll] + summary: statusUserKeyRsa + operationId: statusUserKeyRsa + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + - name: keyType + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/create: + post: + tags: [CryptoAll] + summary: createFile + operationId: createFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileCreateInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/delete: + post: + tags: [CryptoAll] + summary: deleteFile + operationId: deleteFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileDeleteInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/read: + post: + tags: [CryptoAll] + summary: readFile + operationId: readFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileReadInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/file/write: + post: + tags: [CryptoAll] + summary: writeFile + operationId: writeFile + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoFileWriteInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/hmac: + post: + tags: [CryptoAll] + summary: hmac + operationId: hmac + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoHmacInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/kek/generate: + post: + tags: [CryptoAll] + summary: generateKek + operationId: generateKek + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateKekInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/kek/status: + get: + tags: [CryptoAll] + summary: kekStatus + operationId: kekStatus + parameters: + - name: keyIndex + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/lmk/check: + get: + tags: [CryptoAll] + summary: checkLmk + operationId: checkLmk + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/lmk/seed-mac: + get: + tags: [CryptoAll] + summary: exportLmkSeedMac + operationId: exportLmkSeedMac + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/mac/plain: + post: + tags: [CryptoAll] + summary: macPlain + operationId: macPlain + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoMacInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/random: + get: + tags: [CryptoAll] + summary: random + operationId: random + parameters: + - name: length + in: query + required: true + schema: + type: integer + format: int32 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/self-test: + post: + tags: [CryptoAll] + summary: selfTest + operationId: selfTest + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/std/keypair/ecc: + post: + tags: [CryptoAll] + summary: generateStandardEccKeyPair + operationId: generateStandardEccKeyPair + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateStandardEccKeyPairInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + + /api/v1/device/crypto/std/keypair/rsa: + post: + tags: [CryptoAll] + summary: generateStandardRsaKeyPair + operationId: generateStandardRsaKeyPair + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CryptoGenerateStandardRsaKeyPairInternalRequest' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalError' + +components: + securitySchemes: + InternalTokenAuth: + type: apiKey + in: header + name: X-Internal-Token + + responses: + BadRequest: + description: 参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + InternalError: + description: 业务/系统错误(含密码卡调用失败) + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponseAny' + + schemas: + ApiResponseAny: + type: object + properties: + success: + type: boolean + code: + type: integer + msg: + type: string + data: + type: object + additionalProperties: true + nullable: true + timestamp: + type: integer + format: int64 + traceId: + type: string + nullable: true + path: + type: string + nullable: true + required: [success, code, msg, timestamp] + + Base64DataRequest: + type: object + additionalProperties: true + + Base64PairRequest: + type: object + additionalProperties: true + + ChangePrivateKeyPasswordRequest: + type: object + additionalProperties: true + + CryptoDigestInternalRequest: + type: object + additionalProperties: true + + CryptoExchangeEnvelopeEccInternalRequest: + type: object + additionalProperties: true + + CryptoExchangeEnvelopeRsaInternalRequest: + type: object + additionalProperties: true + + CryptoFileCreateInternalRequest: + type: object + additionalProperties: true + + CryptoFileDeleteInternalRequest: + type: object + additionalProperties: true + + CryptoFileReadInternalRequest: + type: object + additionalProperties: true + + CryptoFileWriteInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateAgreementDataAndKeyEccInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateKekInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateSessionKeyEccInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateStandardEccKeyPairInternalRequest: + type: object + additionalProperties: true + + CryptoGenerateStandardRsaKeyPairInternalRequest: + type: object + additionalProperties: true + + CryptoHmacInternalRequest: + type: object + additionalProperties: true + + CryptoMacInternalRequest: + type: object + additionalProperties: true + + CryptoSymmetricInternalRequest: + type: object + additionalProperties: true + + CryptoSymmetricKekInternalRequest: + type: object + additionalProperties: true + + EccExternalDecryptRequest: + type: object + additionalProperties: true + + EccExternalEncryptRequest: + type: object + additionalProperties: true + + EccExternalSignRequest: + type: object + additionalProperties: true + + EccExternalVerifyRequest: + type: object + additionalProperties: true + + EccInternalSignRequest: + type: object + additionalProperties: true + + EccInternalVerifyRequest: + type: object + additionalProperties: true + + ExportKekComponentRequest: + type: object + additionalProperties: true + + GenerateKeyPairRequest: + type: object + additionalProperties: true + + GenerateKeyWithEpkEccRequest: + type: object + additionalProperties: true + + GenerateKeyWithEpkRsaRequest: + type: object + additionalProperties: true + + GenerateKeyWithIpkEccRequest: + type: object + additionalProperties: true + + GenerateKeyWithIpkRsaRequest: + type: object + additionalProperties: true + + GenerateKeyWithKekRequest: + type: object + additionalProperties: true + + ImportEncKeyPairEccRequest: + type: object + additionalProperties: true + + ImportEncKeyPairRsaRequest: + type: object + additionalProperties: true + + ImportKekRequest: + type: object + additionalProperties: true + + ImportKeyPairRequest: + type: object + additionalProperties: true + + ImportKeyWithIskEccRequest: + type: object + additionalProperties: true + + ImportKeyWithIskRsaRequest: + type: object + additionalProperties: true + + KekComponentRequest: + type: object + additionalProperties: true + + KeyTypePinRequest: + type: object + additionalProperties: true + + PrivateKeyAccessRequest: + type: object + additionalProperties: true + + RecoverKekRequest: + type: object + additionalProperties: true + + RecoverUserKeyRequest: + type: object + additionalProperties: true + + RsaExternalPrivateKeyOpRequest: + type: object + additionalProperties: true + + RsaExternalPublicKeyOpRequest: + type: object + additionalProperties: true + + RsaInternalKeyOpRequest: + type: object + additionalProperties: true + + UkeyGenerateRequest: + type: object + additionalProperties: true + + UkeyLoginRequest: + type: object + additionalProperties: true + + UkeyPinChangeRequest: + type: object + additionalProperties: true + + UkeyUnlockRequest: + type: object + additionalProperties: true + + UpdateBackedKeyRequest: + type: object + additionalProperties: true diff --git a/pom.xml b/pom.xml index 0d27edd..43d1d20 100644 --- a/pom.xml +++ b/pom.xml @@ -105,6 +105,16 @@ maven-compiler-plugin true + + + org.projectlombok + lombok + + + org.springframework.boot + spring-boot-configuration-processor + + diff --git a/scripts/standard-init/stop_standard_apps.sh b/scripts/standard-init/stop_standard_apps.sh new file mode 100644 index 0000000..909d25f --- /dev/null +++ b/scripts/standard-init/stop_standard_apps.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -euo pipefail + +STANDARD_HOME_DIR="${STANDARD_HOME_DIR:?STANDARD_HOME_DIR is required}" +STANDARD_RUN_USER="${STANDARD_RUN_USER:?STANDARD_RUN_USER is required}" + +if [ -x "${STANDARD_HOME_DIR}/cmsp/stop_cmsp.sh" ]; then + su - "${STANDARD_RUN_USER}" -s /bin/bash "${STANDARD_HOME_DIR}/cmsp/stop_cmsp.sh" || true +else + pkill -f "${STANDARD_HOME_DIR}/cmsp" >/dev/null 2>&1 || true +fi + +if [ -x "${STANDARD_HOME_DIR}/cmtp/stop_cmtp.sh" ]; then + su - "${STANDARD_RUN_USER}" -s /bin/bash "${STANDARD_HOME_DIR}/cmtp/stop_cmtp.sh" || true +else + pkill -f "${STANDARD_HOME_DIR}/cmtp" >/dev/null 2>&1 || true +fi diff --git a/scripts/standard-init/stop_standard_nginx.sh b/scripts/standard-init/stop_standard_nginx.sh new file mode 100644 index 0000000..6631278 --- /dev/null +++ b/scripts/standard-init/stop_standard_nginx.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +if pgrep -f "/usr/local/nginx/sbin/nginx" >/dev/null; then + /usr/local/nginx/sbin/nginx -s stop || pkill -f "/usr/local/nginx/sbin/nginx" >/dev/null 2>&1 || true +fi diff --git a/scripts/standard-init/stop_standard_rabbitmq.sh b/scripts/standard-init/stop_standard_rabbitmq.sh new file mode 100644 index 0000000..e578989 --- /dev/null +++ b/scripts/standard-init/stop_standard_rabbitmq.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail + +systemctl stop rabbitmq-server >/dev/null 2>&1 || true diff --git a/scripts/tms.sh b/scripts/tms.sh index 942f587..8421e1a 100755 --- a/scripts/tms.sh +++ b/scripts/tms.sh @@ -22,7 +22,7 @@ PID_FILE="${PID_FILE:-${RUN_DIR}/${APP_NAME}.pid}" OUT_LOG="${OUT_LOG:-${LOG_DIR}/${APP_NAME}.out.log}" ERR_LOG="${ERR_LOG:-${LOG_DIR}/${APP_NAME}.err.log}" -SPRING_PROFILES_ACTIVE="${SPRING_PROFILES_ACTIVE:-prod}" +SPRING_PROFILES_ACTIVE="${SPRING_PROFILES_ACTIVE:-}" JAVA_BIN="${JAVA_BIN:-java}" JAVA_OPTS="${JAVA_OPTS:- -Xms512m -Xmx1024m}" ALLOW_MISSING_EXTERNAL_CONFIG="${ALLOW_MISSING_EXTERNAL_CONFIG:-false}" @@ -87,15 +87,27 @@ start_app() { echo "[INFO] starting ${APP_NAME}" echo "[INFO] app home: ${APP_HOME}" echo "[INFO] jar path: ${JAR_PATH}" - echo "[INFO] profile: ${SPRING_PROFILES_ACTIVE}" + if [[ -n "${SPRING_PROFILES_ACTIVE}" ]]; then + echo "[INFO] profile override: ${SPRING_PROFILES_ACTIVE}" + else + echo "[INFO] profile override: " + fi echo "[INFO] external config dir: ${CONF_DIR}" - # shellcheck disable=SC2086 - nohup "${JAVA_BIN}" ${JAVA_OPTS} \ - -jar "${JAR_PATH}" \ - --spring.profiles.active="${SPRING_PROFILES_ACTIVE}" \ - --spring.config.additional-location="${cfg_location}" \ - >"${OUT_LOG}" 2>"${ERR_LOG}" & + if [[ -n "${SPRING_PROFILES_ACTIVE}" ]]; then + # shellcheck disable=SC2086 + nohup "${JAVA_BIN}" ${JAVA_OPTS} \ + -jar "${JAR_PATH}" \ + --spring.profiles.active="${SPRING_PROFILES_ACTIVE}" \ + --spring.config.additional-location="${cfg_location}" \ + >"${OUT_LOG}" 2>"${ERR_LOG}" & + else + # shellcheck disable=SC2086 + nohup "${JAVA_BIN}" ${JAVA_OPTS} \ + -jar "${JAR_PATH}" \ + --spring.config.additional-location="${cfg_location}" \ + >"${OUT_LOG}" 2>"${ERR_LOG}" & + fi local pid="$!" echo "${pid}" > "${PID_FILE}" @@ -160,7 +172,7 @@ Environment variables: CONF_DIR default: /config RUN_DIR default: /run LOG_DIR default: /logs - SPRING_PROFILES_ACTIVE default: prod + SPRING_PROFILES_ACTIVE optional: override Spring profile JAVA_BIN default: java JAVA_OPTS default: -Xms512m -Xmx1024m ALLOW_MISSING_EXTERNAL_CONFIG default: false diff --git a/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java b/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java index aee63a7..38b3d5a 100644 --- a/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java +++ b/src/main/java/com/cisd/tms/common/config/WebMvcConfig.java @@ -1,5 +1,7 @@ package com.cisd.tms.common.config; +import com.cisd.tms.common.config.properties.TmsCorsProperties; +import com.cisd.tms.modules.auth.security.InternalAuthorizationInterceptor; import com.cisd.tms.security.internal.InternalApiAuthInterceptor; import com.cisd.tms.security.openapi.OpenApiSignAuthInterceptor; import com.cisd.tms.common.util.TraceIdUtil; @@ -12,6 +14,7 @@ import org.slf4j.MDC; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -19,14 +22,20 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; public class WebMvcConfig implements WebMvcConfigurer { private final InternalApiAuthInterceptor internalApiAuthInterceptor; + private final InternalAuthorizationInterceptor internalAuthorizationInterceptor; private final OpenApiSignAuthInterceptor openApiSignAuthInterceptor; + private final TmsCorsProperties corsProperties; public WebMvcConfig( InternalApiAuthInterceptor internalApiAuthInterceptor, - OpenApiSignAuthInterceptor openApiSignAuthInterceptor + InternalAuthorizationInterceptor internalAuthorizationInterceptor, + OpenApiSignAuthInterceptor openApiSignAuthInterceptor, + TmsCorsProperties corsProperties ) { this.internalApiAuthInterceptor = internalApiAuthInterceptor; + this.internalAuthorizationInterceptor = internalAuthorizationInterceptor; this.openApiSignAuthInterceptor = openApiSignAuthInterceptor; + this.corsProperties = corsProperties; } @Bean @@ -56,10 +65,31 @@ public class WebMvcConfig implements WebMvcConfigurer { .addPathPatterns("/api/**") .excludePathPatterns( "/api/v1/device/status", - "/api/v1/auth/login" + "/api/v1/auth/login", + "/api/v1/auth/password-login", + "/api/v1/auth/ukey-login", + "/api/v1/auth/ukey-login/randoms" ); + registry.addInterceptor(internalAuthorizationInterceptor) + .addPathPatterns("/api/**"); + registry.addInterceptor(openApiSignAuthInterceptor) .addPathPatterns("/openapi/**"); } + + @Override + public void addCorsMappings(CorsRegistry registry) { + if (!corsProperties.isEnabled() || corsProperties.getAllowedOriginPatterns().isEmpty()) { + return; + } + + registry.addMapping("/**") + .allowedOriginPatterns(corsProperties.getAllowedOriginPatterns().toArray(String[]::new)) + .allowedMethods(corsProperties.getAllowedMethods().toArray(String[]::new)) + .allowedHeaders(corsProperties.getAllowedHeaders().toArray(String[]::new)) + .exposedHeaders(corsProperties.getExposedHeaders().toArray(String[]::new)) + .allowCredentials(corsProperties.isAllowCredentials()) + .maxAge(corsProperties.getMaxAgeSeconds()); + } } diff --git a/src/main/java/com/cisd/tms/common/config/properties/TmsCorsProperties.java b/src/main/java/com/cisd/tms/common/config/properties/TmsCorsProperties.java new file mode 100644 index 0000000..f510aec --- /dev/null +++ b/src/main/java/com/cisd/tms/common/config/properties/TmsCorsProperties.java @@ -0,0 +1,94 @@ +package com.cisd.tms.common.config.properties; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "tms.web.cors") +public class TmsCorsProperties { + + private boolean enabled = true; + private List allowedOriginPatterns = new ArrayList<>(List.of( + "http://localhost:*", + "http://127.0.0.1:*" + )); + private List allowedMethods = new ArrayList<>(List.of( + "GET", + "POST", + "PUT", + "DELETE", + "OPTIONS" + )); + private List allowedHeaders = new ArrayList<>(List.of( + "Authorization", + "Content-Type", + "X-Requested-With", + "X-Internal-Token", + "X-App-Id", + "X-Timestamp", + "X-Nonce", + "X-Signature", + "X-Trace-Id" + )); + private List exposedHeaders = new ArrayList<>(List.of( + "X-Trace-Id" + )); + private boolean allowCredentials = true; + private long maxAgeSeconds = 3600; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public List getAllowedOriginPatterns() { + return allowedOriginPatterns; + } + + public void setAllowedOriginPatterns(List allowedOriginPatterns) { + this.allowedOriginPatterns = allowedOriginPatterns; + } + + public List getAllowedMethods() { + return allowedMethods; + } + + public void setAllowedMethods(List allowedMethods) { + this.allowedMethods = allowedMethods; + } + + public List getAllowedHeaders() { + return allowedHeaders; + } + + public void setAllowedHeaders(List allowedHeaders) { + this.allowedHeaders = allowedHeaders; + } + + public List getExposedHeaders() { + return exposedHeaders; + } + + public void setExposedHeaders(List exposedHeaders) { + this.exposedHeaders = exposedHeaders; + } + + public boolean isAllowCredentials() { + return allowCredentials; + } + + public void setAllowCredentials(boolean allowCredentials) { + this.allowCredentials = allowCredentials; + } + + public long getMaxAgeSeconds() { + return maxAgeSeconds; + } + + public void setMaxAgeSeconds(long maxAgeSeconds) { + this.maxAgeSeconds = maxAgeSeconds; + } +} diff --git a/src/main/java/com/cisd/tms/common/config/properties/TmsSecurityProperties.java b/src/main/java/com/cisd/tms/common/config/properties/TmsSecurityProperties.java index 4b8c50f..e9dffef 100644 --- a/src/main/java/com/cisd/tms/common/config/properties/TmsSecurityProperties.java +++ b/src/main/java/com/cisd/tms/common/config/properties/TmsSecurityProperties.java @@ -8,6 +8,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; public class TmsSecurityProperties { private String internalToken = "change-me-internal-token"; + private final InternalAuth internalAuth = new InternalAuth(); private final Openapi openapi = new Openapi(); public String getInternalToken() { @@ -18,10 +19,53 @@ public class TmsSecurityProperties { this.internalToken = internalToken; } + public InternalAuth getInternalAuth() { + return internalAuth; + } + public Openapi getOpenapi() { return openapi; } + public static class InternalAuth { + private boolean enabled = true; + private String debugRoleCode = "KEY_ADMIN"; + private String debugAuthLevel = "FULL"; + private String debugSessionToken = "DEBUG-BYPASS"; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getDebugRoleCode() { + return debugRoleCode; + } + + public void setDebugRoleCode(String debugRoleCode) { + this.debugRoleCode = debugRoleCode; + } + + public String getDebugAuthLevel() { + return debugAuthLevel; + } + + public void setDebugAuthLevel(String debugAuthLevel) { + this.debugAuthLevel = debugAuthLevel; + } + + public String getDebugSessionToken() { + return debugSessionToken; + } + + public void setDebugSessionToken(String debugSessionToken) { + this.debugSessionToken = debugSessionToken; + } + } + public static class Openapi { private long timestampSkewSeconds = 300; private Map clients = new HashMap<>(); diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplate.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplate.java index bdcb99d..97f66ab 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplate.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplate.java @@ -7,6 +7,7 @@ import com.sun.jna.Memory; import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.ptr.PointerByReference; +import jakarta.annotation.PreDestroy; import java.util.HashMap; import java.util.Map; import org.slf4j.Logger; @@ -33,6 +34,7 @@ public class PcieSessionTemplate { private final Object lock = new Object(); private volatile PcieNativeLibrary nativeLibrary; + private volatile Pointer sharedDeviceHandle; public PcieSessionTemplate(CryptoCardProperties properties, PcieErrorMapper errorMapper) { this.properties = properties; @@ -42,32 +44,22 @@ public class PcieSessionTemplate { public T withDevice(String apiName, DeviceCallback callback) { synchronized (lock) { PcieNativeLibrary lib = ensureLibraryLoaded(); - - PointerByReference deviceRef = new PointerByReference(); - int openRc = lib.SDF_OpenDevice(deviceRef); - ensureSuccess(apiName + ":SDF_OpenDevice", openRc); - - Pointer device = deviceRef.getValue(); - try { - return callback.apply(lib, device); - } finally { - int closeRc = lib.SDF_CloseDevice(device); - if (closeRc != 0) { - log.warn("{} close device failed: {}", apiName, errorMapper.toMessage(closeRc)); - } - } + Pointer device = ensureSharedDeviceOpened(apiName, lib); + return callback.apply(lib, device); } } public T withSession(String apiName, SessionCallback callback) { - return withDevice(apiName, (lib, deviceHandle) -> { - PointerByReference sessionRef = new PointerByReference(); - Pointer ph = new Memory(8L); - sessionRef.setValue(ph); - int openSessionRc = lib.SDF_OpenSession(deviceHandle, sessionRef); - ensureSuccess(apiName + ":SDF_OpenSession", openSessionRc); + synchronized (lock) { + PcieNativeLibrary lib = ensureLibraryLoaded(); + Pointer deviceHandle = ensureSharedDeviceOpened(apiName, lib); + Pointer session = tryOpenSession(apiName, lib, deviceHandle); + if (session == null) { + resetSharedDevice(lib); + deviceHandle = ensureSharedDeviceOpened(apiName, lib); + session = openSessionOrThrow(apiName, lib, deviceHandle); + } - Pointer session = sessionRef.getValue(); try { return callback.apply(lib, deviceHandle, session); } finally { @@ -76,7 +68,7 @@ public class PcieSessionTemplate { log.warn("{} close session failed: {}", apiName, errorMapper.toMessage(closeSessionRc)); } } - }); + } } public void ensureSuccess(String apiName, int retCode) { @@ -85,6 +77,13 @@ public class PcieSessionTemplate { } } + @PreDestroy + void closeSharedDevice() { + synchronized (lock) { + closeSharedDevice(libOrNull()); + } + } + private PcieNativeLibrary ensureLibraryLoaded() { PcieNativeLibrary cached = nativeLibrary; if (cached != null) { @@ -116,4 +115,55 @@ public class PcieSessionTemplate { return nativeLibrary; } } + + private Pointer ensureSharedDeviceOpened(String apiName, PcieNativeLibrary lib) { + Pointer cached = sharedDeviceHandle; + if (cached != null) { + return cached; + } + + PointerByReference deviceRef = new PointerByReference(); + int openRc = lib.SDF_OpenDevice(deviceRef); + ensureSuccess(apiName + ":SDF_OpenDevice", openRc); + sharedDeviceHandle = deviceRef.getValue(); + return sharedDeviceHandle; + } + + private Pointer tryOpenSession(String apiName, PcieNativeLibrary lib, Pointer deviceHandle) { + PointerByReference sessionRef = new PointerByReference(); + int openSessionRc = lib.SDF_OpenSession(deviceHandle, sessionRef); + if (openSessionRc == 0) { + return sessionRef.getValue(); + } + log.warn("{} open session failed, will reset shared device and retry once: {}", apiName, errorMapper.toMessage(openSessionRc)); + return null; + } + + private Pointer openSessionOrThrow(String apiName, PcieNativeLibrary lib, Pointer deviceHandle) { + PointerByReference sessionRef = new PointerByReference(); + int openSessionRc = lib.SDF_OpenSession(deviceHandle, sessionRef); + ensureSuccess(apiName + ":SDF_OpenSession", openSessionRc); + return sessionRef.getValue(); + } + + private void resetSharedDevice(PcieNativeLibrary lib) { + closeSharedDevice(lib); + } + + private void closeSharedDevice(PcieNativeLibrary lib) { + Pointer device = sharedDeviceHandle; + if (device == null || lib == null) { + return; + } + + sharedDeviceHandle = null; + int closeRc = lib.SDF_CloseDevice(device); + if (closeRc != 0) { + log.warn("close shared device failed: {}", errorMapper.toMessage(closeRc)); + } + } + + private PcieNativeLibrary libOrNull() { + return nativeLibrary; + } } diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java index a69d744..8ac0635 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/PcieNativeLibrary.java @@ -154,7 +154,7 @@ public interface PcieNativeLibrary extends Library { int SDFE_InitIdentify(Pointer hSessionHandle, byte[] oldPin, int oldPinLen, byte[] newPin, int newPinLen); - int SDFE_DeviceStatusGet(Pointer hSessionHandle, Pointer pDeviceStatus); + int SDFE_DeviceStatusGet(Pointer hSessionHandle, SdfeDeviceStatus deviceStatus); int SDFE_DeviceSelfTest(Pointer hSessionHandle); @@ -245,15 +245,15 @@ public interface PcieNativeLibrary extends Library { int SDFE_UnlockUkey(Pointer hSessionHandle, byte[] adminPin, int adminPinLen, byte[] userPin, int userPinLen); - int SDFE_ReadLMKComponent(Pointer hSessionHandle, byte[] ukeyPin, int ukeyPinLen); + int SDFE_ReadUkeyLMKComponent(Pointer hSessionHandle, byte[] ukeyPin, int ukeyPinLen); int SDFE_ReadUkeyIKComponent(Pointer hSessionHandle, int keyType, byte[] ukeyPin, int ukeyPinLen); - int SDFE_GenerateCertificate(Pointer hSessionHandle, byte[] ukeyPin, int ukeyPinLen); + int SDFE_GenerateUkeyCertificate(Pointer hSessionHandle, byte[] ukeyPin, int ukeyPinLen); - int SDFE_LoginoutAdmin(Pointer hSessionHandle, byte adminIndex); + int SDFE_LoginoutUkey(Pointer hSessionHandle, byte adminIndex); - int SDFE_LoginStatus(Pointer hSessionHandle, SdfeLoginState loginState); + int SDFE_UkeyLoginStatus(Pointer hSessionHandle, SdfeLoginState loginState); int SDFE_HmacInit(Pointer hSessionHandle, int algId, byte[] key, int keyLen); diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/SdfeDeviceConf.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/SdfeDeviceConf.java index 7a19b64..9272171 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/SdfeDeviceConf.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/SdfeDeviceConf.java @@ -3,6 +3,7 @@ package com.cisd.tms.integration.crypto.pcie.jna; import com.sun.jna.Structure; @Structure.FieldOrder({ + "type", "level", "devCount", "rsaKeyCount", @@ -13,6 +14,7 @@ import com.sun.jna.Structure; }) public class SdfeDeviceConf extends Structure { + public int type; public int level; public int devCount; public int rsaKeyCount; diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/SdfeDeviceStatus.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/SdfeDeviceStatus.java new file mode 100644 index 0000000..7bb8f3b --- /dev/null +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/jna/SdfeDeviceStatus.java @@ -0,0 +1,51 @@ +package com.cisd.tms.integration.crypto.pcie.jna; + +import com.sun.jna.Structure; + +@Structure.FieldOrder({ + "fsmState", + "dkeyState", + "errorCode", + "version", + "commit", + "frequency", + "pcieSpeed", + "pcieWidth", + "chipTemp", + "chipVoltage", + "boardTemp", + "boardVoltage", + "boardCurrent", + "chipTempmin", + "chipTempmax", + "boardVolmin", + "boardVolmax", + "fsTotalKb", + "fsFreeKb" +}) +public class SdfeDeviceStatus extends Structure { + + public int fsmState; + public int dkeyState; + public int errorCode; + public int version; + public int commit; + public int frequency; + public int pcieSpeed; + public int pcieWidth; + public float chipTemp; + public float chipVoltage; + public float boardTemp; + public float boardVoltage; + public float boardCurrent; + public float chipTempmin; + public float chipTempmax; + public float boardVolmin; + public float boardVolmax; + public int fsTotalKb; + public int fsFreeKb; + + public SdfeDeviceStatus() { + super(ALIGN_NONE); + } +} diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceConfResult.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceConfResult.java index 4169d11..e8b4342 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceConfResult.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceConfResult.java @@ -2,6 +2,7 @@ package com.cisd.tms.integration.crypto.pcie.model; public class DeviceConfResult { + private int type; private int level; private int devCount; private int rsaKeyCount; @@ -10,6 +11,14 @@ public class DeviceConfResult { private int sessKeyCount; private int fileSysCapacity; + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + public int getLevel() { return level; } diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceInfoResult.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceInfoResult.java index ba7e5ae..20f9fda 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceInfoResult.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceInfoResult.java @@ -12,6 +12,13 @@ public class DeviceInfoResult { private int symAlgAbility; private int hashAlgAbility; private int bufferSize; + private String deviceVersionText; + private String standardVersionText; + private String asymmetricAlgorithmsText; + private String asymmetricKeyBitsText; + private String symmetricAlgorithmsText; + private String hashAlgorithmsText; + private String bufferSizeText; public String getIssuerName() { return issuerName; @@ -92,4 +99,60 @@ public class DeviceInfoResult { public void setBufferSize(int bufferSize) { this.bufferSize = bufferSize; } + + public String getDeviceVersionText() { + return deviceVersionText; + } + + public void setDeviceVersionText(String deviceVersionText) { + this.deviceVersionText = deviceVersionText; + } + + public String getStandardVersionText() { + return standardVersionText; + } + + public void setStandardVersionText(String standardVersionText) { + this.standardVersionText = standardVersionText; + } + + public String getAsymmetricAlgorithmsText() { + return asymmetricAlgorithmsText; + } + + public void setAsymmetricAlgorithmsText(String asymmetricAlgorithmsText) { + this.asymmetricAlgorithmsText = asymmetricAlgorithmsText; + } + + public String getAsymmetricKeyBitsText() { + return asymmetricKeyBitsText; + } + + public void setAsymmetricKeyBitsText(String asymmetricKeyBitsText) { + this.asymmetricKeyBitsText = asymmetricKeyBitsText; + } + + public String getSymmetricAlgorithmsText() { + return symmetricAlgorithmsText; + } + + public void setSymmetricAlgorithmsText(String symmetricAlgorithmsText) { + this.symmetricAlgorithmsText = symmetricAlgorithmsText; + } + + public String getHashAlgorithmsText() { + return hashAlgorithmsText; + } + + public void setHashAlgorithmsText(String hashAlgorithmsText) { + this.hashAlgorithmsText = hashAlgorithmsText; + } + + public String getBufferSizeText() { + return bufferSizeText; + } + + public void setBufferSizeText(String bufferSizeText) { + this.bufferSizeText = bufferSizeText; + } } diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceStatusResult.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceStatusResult.java index e5a5dbe..68b884c 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceStatusResult.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/model/DeviceStatusResult.java @@ -1,9 +1,30 @@ package com.cisd.tms.integration.crypto.pcie.model; +import com.fasterxml.jackson.annotation.JsonIgnore; + public class DeviceStatusResult { + @JsonIgnore private byte[] statusData; private int length; + private String statusHex; + private int fsmState; + private String fsmStateText; + private int commit; + private String commitHex; + private int frequencyMhz; + private String frequencyText; + private int pcieSpeed; + private int pcieWidth; + private String pcieInfoText; + private double boardTempC; + private String boardTempText; + private double boardVoltageV; + private String boardVoltageText; + private int fsTotalKb; + private String fsTotalText; + private int fsFreeKb; + private String fsFreeText; public byte[] getStatusData() { return statusData; @@ -20,4 +41,148 @@ public class DeviceStatusResult { public void setLength(int length) { this.length = length; } + + public String getStatusHex() { + return statusHex; + } + + public void setStatusHex(String statusHex) { + this.statusHex = statusHex; + } + + public int getFsmState() { + return fsmState; + } + + public void setFsmState(int fsmState) { + this.fsmState = fsmState; + } + + public String getFsmStateText() { + return fsmStateText; + } + + public void setFsmStateText(String fsmStateText) { + this.fsmStateText = fsmStateText; + } + + public int getCommit() { + return commit; + } + + public void setCommit(int commit) { + this.commit = commit; + } + + public String getCommitHex() { + return commitHex; + } + + public void setCommitHex(String commitHex) { + this.commitHex = commitHex; + } + + public int getFrequencyMhz() { + return frequencyMhz; + } + + public void setFrequencyMhz(int frequencyMhz) { + this.frequencyMhz = frequencyMhz; + } + + public String getFrequencyText() { + return frequencyText; + } + + public void setFrequencyText(String frequencyText) { + this.frequencyText = frequencyText; + } + + public int getPcieSpeed() { + return pcieSpeed; + } + + public void setPcieSpeed(int pcieSpeed) { + this.pcieSpeed = pcieSpeed; + } + + public int getPcieWidth() { + return pcieWidth; + } + + public void setPcieWidth(int pcieWidth) { + this.pcieWidth = pcieWidth; + } + + public String getPcieInfoText() { + return pcieInfoText; + } + + public void setPcieInfoText(String pcieInfoText) { + this.pcieInfoText = pcieInfoText; + } + + public double getBoardTempC() { + return boardTempC; + } + + public void setBoardTempC(double boardTempC) { + this.boardTempC = boardTempC; + } + + public String getBoardTempText() { + return boardTempText; + } + + public void setBoardTempText(String boardTempText) { + this.boardTempText = boardTempText; + } + + public double getBoardVoltageV() { + return boardVoltageV; + } + + public void setBoardVoltageV(double boardVoltageV) { + this.boardVoltageV = boardVoltageV; + } + + public String getBoardVoltageText() { + return boardVoltageText; + } + + public void setBoardVoltageText(String boardVoltageText) { + this.boardVoltageText = boardVoltageText; + } + + public int getFsTotalKb() { + return fsTotalKb; + } + + public void setFsTotalKb(int fsTotalKb) { + this.fsTotalKb = fsTotalKb; + } + + public String getFsTotalText() { + return fsTotalText; + } + + public void setFsTotalText(String fsTotalText) { + this.fsTotalText = fsTotalText; + } + + public int getFsFreeKb() { + return fsFreeKb; + } + + public void setFsFreeKb(int fsFreeKb) { + this.fsFreeKb = fsFreeKb; + } + + public String getFsFreeText() { + return fsFreeText; + } + + public void setFsFreeText(String fsFreeText) { + this.fsFreeText = fsFreeText; + } } diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java index 4118dc6..aa517dd 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoService.java @@ -16,6 +16,7 @@ import com.cisd.tms.integration.crypto.pcie.jna.RsaRefPrivateKey; import com.cisd.tms.integration.crypto.pcie.jna.RsaRefPublicKey; import com.cisd.tms.integration.crypto.pcie.jna.SdfDeviceInfo; import com.cisd.tms.integration.crypto.pcie.jna.SdfeDeviceConf; +import com.cisd.tms.integration.crypto.pcie.jna.SdfeDeviceStatus; import com.cisd.tms.integration.crypto.pcie.jna.SdfeKekComponent; import com.cisd.tms.integration.crypto.pcie.jna.SdfeLmkComponent; import com.cisd.tms.integration.crypto.pcie.jna.SdfeLoginState; @@ -129,6 +130,7 @@ public class JnaPcieCryptoService implements PcieCryptoService { conf.read(); DeviceConfResult result = new DeviceConfResult(); + result.setType(conf.type); result.setLevel(conf.level); result.setDevCount(conf.devCount); result.setRsaKeyCount(conf.rsaKeyCount); @@ -158,6 +160,7 @@ public class JnaPcieCryptoService implements PcieCryptoService { result.setSymAlgAbility(info.symAlgAbility); result.setHashAlgAbility(info.hashAlgAbility); result.setBufferSize(info.bufferSize); + PcieDeviceInfoFormatter.enrich(result); return result; }); } @@ -192,6 +195,40 @@ public class JnaPcieCryptoService implements PcieCryptoService { } } + @Override + public byte[] initializeMasterKey(byte[] oldPin, byte[] newPin) { + byte[] oldPinValue = requireNonEmptyBytes("oldPin", oldPin); + byte[] newPinValue = requireNonEmptyBytes("newPin", newPin); + + try { + return sessionTemplate.withSession("SDFE_InitializeMasterKey", (lib, deviceHandle, sessionHandle) -> { + sessionTemplate.ensureSuccess( + "SDFE_InitIdentify", + lib.SDFE_InitIdentify(sessionHandle, oldPinValue, oldPinValue.length, newPinValue, newPinValue.length) + ); + sessionTemplate.ensureSuccess("SDFE_GenerateLMK", lib.SDFE_GenerateLMK(sessionHandle)); + sessionTemplate.ensureSuccess("SDFE_LoadLMK", lib.SDFE_LoadLMK(sessionHandle)); + sessionTemplate.ensureSuccess("SDFE_GenerateIK", lib.SDFE_GenerateIK(sessionHandle, IK_KEY_TYPE_DEVICE)); + sessionTemplate.ensureSuccess("SDFE_GenerateIK", lib.SDFE_GenerateIK(sessionHandle, IK_KEY_TYPE_AUTH)); + + int retCode = lib.SDFE_CheckLMK(sessionHandle); + if (retCode != 0) { + if ((retCode & 0xFF) == SDFE_INIT_STATUS_LOW_BYTE) { + throw new IllegalStateException("master key initialization failed"); + } + sessionTemplate.ensureSuccess("SDFE_CheckLMK", retCode); + } + + byte[] mac = new byte[LMK_SEED_MAC_LENGTH]; + sessionTemplate.ensureSuccess("SDFE_ExportLMKSeedMAC", lib.SDFE_ExportLMKSeedMAC(sessionHandle, mac)); + return Arrays.copyOf(mac, mac.length); + }); + } finally { + wipe(oldPinValue); + wipe(newPinValue); + } + } + @Override public void deviceSelfTest() { sessionTemplate.withSession("SDFE_DeviceSelfTest", (lib, deviceHandle, sessionHandle) -> { @@ -202,15 +239,16 @@ public class JnaPcieCryptoService implements PcieCryptoService { @Override public DeviceStatusResult getDeviceStatus() { - int size = 8; - return sessionTemplate.withSession("SDFE_DeviceStatusGet", (lib, deviceHandle, sessionHandle) -> { - Memory statusMemory = new Memory(8); - sessionTemplate.ensureSuccess("SDFE_DeviceStatusGet", lib.SDFE_DeviceStatusGet(sessionHandle, statusMemory)); + SdfeDeviceStatus status = new SdfeDeviceStatus(); + sessionTemplate.ensureSuccess("SDFE_DeviceStatusGet", lib.SDFE_DeviceStatusGet(sessionHandle, status)); + status.read(); + int size = status.size(); DeviceStatusResult result = new DeviceStatusResult(); -// result.setLength(size); - result.setStatusData(statusMemory.getByteArray(0, size)); + result.setLength(size); + result.setStatusData(status.getPointer().getByteArray(0, size)); + PcieDeviceStatusFormatter.enrich(result, status); return result; }); } @@ -1611,15 +1649,9 @@ public class JnaPcieCryptoService implements PcieCryptoService { } @Override - public BackupDataResult backupLmkEx(int outBufferSize) { - int size = requirePositive("outBufferSize", outBufferSize); - + public BackupDataResult backupLmkEx() { return sessionTemplate.withSession("SDFE_BackupLMK_EX", (lib, deviceHandle, sessionHandle) -> { SdfeLmkComponent output = new SdfeLmkComponent(); - int structSize = output.size(); - if (size < structSize) { - throw new IllegalArgumentException("outBufferSize must be >= " + structSize); - } sessionTemplate.ensureSuccess("SDFE_BackupLMK_EX", lib.SDFE_BackupLMK_EX(sessionHandle, output)); byte[] outputBytes = toBytes(output); return toBackupResult("SDFE_BackupLMK_EX", outputBytes, outputBytes.length); @@ -1781,8 +1813,11 @@ public class JnaPcieCryptoService implements PcieCryptoService { byte[] pin = requireNonEmptyBytes("ukeyPin", ukeyPin); try { - sessionTemplate.withSession("SDFE_ReadLMKComponent", (lib, deviceHandle, sessionHandle) -> { - sessionTemplate.ensureSuccess("SDFE_ReadLMKComponent", lib.SDFE_ReadLMKComponent(sessionHandle, pin, pin.length)); + sessionTemplate.withSession("SDFE_ReadUkeyLMKComponent", (lib, deviceHandle, sessionHandle) -> { + sessionTemplate.ensureSuccess( + "SDFE_ReadUkeyLMKComponent", + lib.SDFE_ReadUkeyLMKComponent(sessionHandle, pin, pin.length) + ); return null; }); } finally { @@ -1813,8 +1848,11 @@ public class JnaPcieCryptoService implements PcieCryptoService { byte[] pin = requireNonEmptyBytes("ukeyPin", ukeyPin); try { - sessionTemplate.withSession("SDFE_GenerateCertificate", (lib, deviceHandle, sessionHandle) -> { - sessionTemplate.ensureSuccess("SDFE_GenerateCertificate", lib.SDFE_GenerateCertificate(sessionHandle, pin, pin.length)); + sessionTemplate.withSession("SDFE_GenerateUkeyCertificate", (lib, deviceHandle, sessionHandle) -> { + sessionTemplate.ensureSuccess( + "SDFE_GenerateUkeyCertificate", + lib.SDFE_GenerateUkeyCertificate(sessionHandle, pin, pin.length) + ); return null; }); } finally { @@ -1826,17 +1864,17 @@ public class JnaPcieCryptoService implements PcieCryptoService { public void logoutAdmin(int adminIndex) { int safeAdminIndex = requireByteRange("adminIndex", adminIndex); - sessionTemplate.withSession("SDFE_LoginoutAdmin", (lib, deviceHandle, sessionHandle) -> { - sessionTemplate.ensureSuccess("SDFE_LoginoutAdmin", lib.SDFE_LoginoutAdmin(sessionHandle, (byte) safeAdminIndex)); + sessionTemplate.withSession("SDFE_LoginoutUkey", (lib, deviceHandle, sessionHandle) -> { + sessionTemplate.ensureSuccess("SDFE_LoginoutUkey", lib.SDFE_LoginoutUkey(sessionHandle, (byte) safeAdminIndex)); return null; }); } @Override public LoginStatusResult loginStatus() { - return sessionTemplate.withSession("SDFE_LoginStatus", (lib, deviceHandle, sessionHandle) -> { + return sessionTemplate.withSession("SDFE_UkeyLoginStatus", (lib, deviceHandle, sessionHandle) -> { SdfeLoginState state = new SdfeLoginState(); - sessionTemplate.ensureSuccess("SDFE_LoginStatus", lib.SDFE_LoginStatus(sessionHandle, state)); + sessionTemplate.ensureSuccess("SDFE_UkeyLoginStatus", lib.SDFE_UkeyLoginStatus(sessionHandle, state)); state.read(); LoginStatusResult result = new LoginStatusResult(); diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java index e362304..9365c64 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/MockPcieCryptoService.java @@ -2,6 +2,7 @@ package com.cisd.tms.integration.crypto.pcie.service; import com.cisd.tms.integration.crypto.pcie.PcieKeyState; import com.cisd.tms.integration.crypto.pcie.PcieUserKeyType; +import com.cisd.tms.integration.crypto.pcie.jna.SdfeDeviceStatus; import com.cisd.tms.integration.crypto.pcie.model.AgreementDataAndKeyEccResult; import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; import com.cisd.tms.integration.crypto.pcie.model.ChangePrivateKeyPasswordRequest; @@ -100,6 +101,19 @@ public class MockPcieCryptoService implements PcieCryptoService { requireNonEmptyBytes("newPin", newPin); } + @Override + public byte[] initializeMasterKey(byte[] oldPin, byte[] newPin) { + initIdentify(oldPin, newPin); + generateLmk(); + loadLmk(); + generateIk(1); + generateIk(2); + if (!checkLmk()) { + throw new IllegalStateException("master key initialization failed"); + } + return exportLmkSeedMac(); + } + @Override public void deviceSelfTest() { // no-op @@ -107,7 +121,7 @@ public class MockPcieCryptoService implements PcieCryptoService { @Override public DeviceStatusResult getDeviceStatus() { - int size = 8; + int size = new SdfeDeviceStatus().size(); DeviceStatusResult result = new DeviceStatusResult(); result.setLength(size); result.setStatusData(new byte[size]); @@ -679,9 +693,8 @@ public class MockPcieCryptoService implements PcieCryptoService { } @Override - public BackupDataResult backupLmkEx(int outBufferSize) { - int size = requirePositive("outBufferSize", outBufferSize); - return mockBackup(size, 0x11); + public BackupDataResult backupLmkEx() { + return mockBackup(new com.cisd.tms.integration.crypto.pcie.jna.SdfeLmkComponent().size(), 0x11); } @Override diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java index cae7ccb..66da9e9 100644 --- a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieCryptoService.java @@ -71,6 +71,8 @@ public interface PcieCryptoService { void initIdentify(byte[] oldPin, byte[] newPin); + byte[] initializeMasterKey(byte[] oldPin, byte[] newPin); + void deviceSelfTest(); DeviceStatusResult getDeviceStatus(); @@ -209,7 +211,7 @@ public interface PcieCryptoService { byte[] exportLmkSeedMac(); - BackupDataResult backupLmkEx(int outBufferSize); + BackupDataResult backupLmkEx(); void recoverLmkEx(byte[] lmkComponent); diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceInfoFormatter.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceInfoFormatter.java new file mode 100644 index 0000000..212e4b2 --- /dev/null +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceInfoFormatter.java @@ -0,0 +1,146 @@ +package com.cisd.tms.integration.crypto.pcie.service; + +import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +final class PcieDeviceInfoFormatter { + + private static final int ASYM_RSA = 0x00010000; + private static final int ASYM_SM2_SIGN = 0x00020200; + private static final int ASYM_SM2_DH = 0x00020400; + private static final int ASYM_SM2_ENC = 0x00020800; + + private static final int SYM_SM1 = 0x00000100; + private static final int SYM_SM4 = 0x00000400; + private static final int SYM_AES128 = 0x00008000; + private static final int SYM_AES192 = 0x00010000; + private static final int SYM_AES256 = 0x00020000; + + private static final int HASH_SM3 = 0x00000001; + private static final int HASH_SHA256 = 0x00000004; + + private static final int ASYM_BITS_256 = 0x00000100; + private static final int ASYM_BITS_2048 = 0x08000000; + + private PcieDeviceInfoFormatter() { + } + + static void enrich(DeviceInfoResult result) { + if (result == null) { + return; + } + + result.setDeviceVersionText(formatDeviceVersion(result.getDeviceVersion())); + result.setStandardVersionText(formatStandardVersion(result.getStandardVersion())); + result.setAsymmetricAlgorithmsText(joinNonEmpty(decodeAsymmetricAlgorithms(result.getAsymAlgAbility0()))); + result.setAsymmetricKeyBitsText(joinNonEmpty(decodeAsymmetricKeyBits(result.getAsymAlgAbility1()))); + result.setSymmetricAlgorithmsText(joinNonEmpty(decodeSymmetricAlgorithms(result.getSymAlgAbility()))); + result.setHashAlgorithmsText(joinNonEmpty(decodeHashAlgorithms(result.getHashAlgAbility()))); + result.setBufferSizeText(formatBufferSize(result.getBufferSize())); + } + + private static String formatDeviceVersion(int rawValue) { + byte[] bytes = ByteBuffer.allocate(Integer.BYTES) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(rawValue) + .array(); + String decoded = new String(bytes, StandardCharsets.US_ASCII) + .replace("\u0000", "") + .trim(); + if (!decoded.isEmpty() && decoded.chars().allMatch(PcieDeviceInfoFormatter::isPrintableVersionChar)) { + return decoded.startsWith("V") ? decoded : "V" + decoded; + } + return "0x" + Integer.toHexString(rawValue); + } + + private static boolean isPrintableVersionChar(int value) { + return (value >= '0' && value <= '9') || value == '.' || value == 'V' || value == 'v'; + } + + private static String formatStandardVersion(int rawValue) { + int standard = (rawValue >>> 16) & 0xFFFF; + int yearHi = (rawValue >>> 8) & 0xFF; + int yearLo = rawValue & 0xFF; + if (standard > 0 && yearHi > 0 && yearLo > 0) { + return String.format("GMT %04x-%02x%02x", standard, yearHi, yearLo).toUpperCase(); + } + return "0x" + Integer.toHexString(rawValue); + } + + private static List decodeAsymmetricAlgorithms(int rawValue) { + List values = new ArrayList<>(); + if ((rawValue & ASYM_SM2_SIGN) == ASYM_SM2_SIGN) { + values.add("SM2_SIGN"); + } + if ((rawValue & ASYM_SM2_DH) == ASYM_SM2_DH) { + values.add("SM2_DH"); + } + if ((rawValue & ASYM_SM2_ENC) == ASYM_SM2_ENC) { + values.add("SM2_ENC"); + } + if ((rawValue & ASYM_RSA) == ASYM_RSA) { + values.add("RSA"); + } + return values; + } + + private static List decodeAsymmetricKeyBits(int rawValue) { + List values = new ArrayList<>(); + if ((rawValue & ASYM_BITS_256) == ASYM_BITS_256) { + values.add("256"); + } + if ((rawValue & ASYM_BITS_2048) == ASYM_BITS_2048) { + values.add("2048"); + } + if (!values.isEmpty()) { + values.add("bits"); + } + return values; + } + + private static List decodeSymmetricAlgorithms(int rawValue) { + List values = new ArrayList<>(); + if ((rawValue & SYM_SM1) == SYM_SM1) { + values.add("SM1"); + } + if ((rawValue & SYM_SM4) == SYM_SM4) { + values.add("SM4"); + } + if ((rawValue & SYM_AES128) == SYM_AES128) { + values.add("AES128"); + } + if ((rawValue & SYM_AES192) == SYM_AES192) { + values.add("AES192"); + } + if ((rawValue & SYM_AES256) == SYM_AES256) { + values.add("AES256"); + } + return values; + } + + private static List decodeHashAlgorithms(int rawValue) { + List values = new ArrayList<>(); + if ((rawValue & HASH_SM3) == HASH_SM3) { + values.add("SM3"); + } + if ((rawValue & HASH_SHA256) == HASH_SHA256) { + values.add("SHA256"); + } + return values; + } + + private static String formatBufferSize(int rawValue) { + if (rawValue > 0 && rawValue % 1024 == 0) { + return (rawValue / 1024) + " KB"; + } + return rawValue + " B"; + } + + private static String joinNonEmpty(List values) { + return String.join(" ", values); + } +} diff --git a/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceStatusFormatter.java b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceStatusFormatter.java new file mode 100644 index 0000000..64297c1 --- /dev/null +++ b/src/main/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceStatusFormatter.java @@ -0,0 +1,81 @@ +package com.cisd.tms.integration.crypto.pcie.service; + +import com.cisd.tms.integration.crypto.pcie.jna.SdfeDeviceStatus; +import com.cisd.tms.integration.crypto.pcie.model.DeviceStatusResult; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.Locale; + +public final class PcieDeviceStatusFormatter { + + private PcieDeviceStatusFormatter() { + } + + static void enrich(DeviceStatusResult result, SdfeDeviceStatus status) { + if (result == null || status == null) { + return; + } + byte[] normalized = normalize(result.getStatusData(), result.getLength()); + result.setLength(normalized.length); + result.setStatusHex(HexFormat.of().formatHex(normalized)); + result.setFsmState(status.fsmState); + result.setFsmStateText(formatFsmState(status.fsmState)); + result.setCommit(status.commit); + result.setCommitHex(String.format(Locale.ROOT, "0x%08x", status.commit)); + result.setFrequencyMhz(status.frequency); + result.setFrequencyText(status.frequency + "M"); + result.setPcieSpeed(status.pcieSpeed); + result.setPcieWidth(status.pcieWidth); + result.setPcieInfoText(formatPcieInfo(status.pcieSpeed, status.pcieWidth)); + result.setBoardTempC(round(status.boardTemp)); + result.setBoardTempText(formatFloat(status.boardTemp, "°C")); + result.setBoardVoltageV(round(status.boardVoltage)); + result.setBoardVoltageText(formatFloat(status.boardVoltage, "V")); + result.setFsTotalKb(status.fsTotalKb); + result.setFsTotalText(status.fsTotalKb + " KB"); + result.setFsFreeKb(status.fsFreeKb); + result.setFsFreeText(status.fsFreeKb + " KB"); + } + + public static void ensureHex(DeviceStatusResult result) { + if (result == null) { + return; + } + if (result.getStatusHex() == null || result.getStatusHex().isBlank()) { + byte[] normalized = normalize(result.getStatusData(), result.getLength()); + result.setLength(normalized.length); + result.setStatusHex(HexFormat.of().formatHex(normalized)); + } + } + + private static String formatFsmState(int fsmState) { + return switch (fsmState) { + case 0 -> "就绪状态"; + case 1 -> "未初始化状态"; + default -> "状态码:" + fsmState; + }; + } + + private static String formatPcieInfo(int speed, int width) { + if (speed <= 0 || width <= 0) { + return ""; + } + return "GEN" + speed + " X" + width; + } + + private static double round(float value) { + return Math.round(value * 100.0d) / 100.0d; + } + + private static String formatFloat(float value, String unit) { + return String.format(Locale.ROOT, "%.2f %s", round(value), unit); + } + + private static byte[] normalize(byte[] data, int length) { + if (data == null || data.length == 0) { + return new byte[0]; + } + int safeLength = length > 0 && length <= data.length ? length : data.length; + return Arrays.copyOf(data, safeLength); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java index 1f8a71a..95d1fdb 100644 --- a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java @@ -1,18 +1,26 @@ package com.cisd.tms.modules.auth.controller; import com.cisd.tms.common.api.ApiResponse; +import com.cisd.tms.modules.auth.dto.ChangePasswordRequest; import com.cisd.tms.modules.auth.dto.CurrentUserResponse; import com.cisd.tms.modules.auth.dto.LoginRequest; import com.cisd.tms.modules.auth.dto.LoginResponse; +import com.cisd.tms.modules.auth.dto.UkeyBindRequest; +import com.cisd.tms.modules.auth.security.RequireAuthLevel; +import com.cisd.tms.modules.auth.security.RequireRole; +import com.cisd.tms.modules.auth.service.AuthAdminService; import com.cisd.tms.modules.auth.service.AuthService; +import com.cisd.tms.modules.mk.dto.UKeySignDTO; +import com.cisd.tms.modules.mk.dto.UKeySignResult; +import com.cisd.tms.security.internal.InternalApiAuthInterceptor; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -22,23 +30,108 @@ import org.springframework.web.bind.annotation.RestController; public class AuthController { private final AuthService authService; + private final AuthAdminService authAdminService; - public AuthController(AuthService authService) { + public AuthController(AuthService authService, AuthAdminService authAdminService) { this.authService = authService; + this.authAdminService = authAdminService; } @PostMapping("/login") - @Operation(summary = "内部用户登录", description = "校验用户名和密码,返回访问令牌及过期时间。") + @Operation(summary = "内部用户登录", description = "校验角色口令,可选校验 UKey 序列号,返回访问令牌和认证等级。") public ApiResponse login(@Valid @RequestBody LoginRequest request) { return ApiResponse.success(authService.login(request)); } @GetMapping("/me") - @Operation(summary = "查询当前用户", description = "根据请求头中的用户标识返回当前登录用户信息。") - public ApiResponse me( - @Parameter(description = "当前用户标识,来自内部鉴权链路透传的 X-User 请求头") - @RequestHeader(value = "X-User", required = false) String user + @Operation(summary = "查询当前用户", description = "根据内部鉴权链路透传的当前角色标识返回当前登录用户信息。") + public ApiResponse me(HttpServletRequest request) { + String user = (String) request.getAttribute(com.cisd.tms.security.internal.InternalApiAuthInterceptor.ATTR_ROLE_CODE); + String sessionToken = (String) request.getAttribute(com.cisd.tms.security.internal.InternalApiAuthInterceptor.ATTR_SESSION_TOKEN); + return ApiResponse.success(authService.me(user, sessionToken)); + } + + @PostMapping("/logout") + @Operation(summary = "退出当前会话", description = "使当前内部管理端会话失效。") + public ApiResponse logout(HttpServletRequest request) { + authService.logout((String) request.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN)); + return ApiResponse.success(); + } + + @PostMapping("/change-password") + @Operation(summary = "修改当前角色口令", description = "基于当前登录会话校验并更新角色口令。") + public ApiResponse changePassword(@Valid @RequestBody ChangePasswordRequest request, HttpServletRequest httpRequest) { + authService.changePassword( + (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN), + request.getCurrentPassword(), + request.getNewPassword() + ); + return ApiResponse.success(); + } + + @PostMapping("/roles/{roleCode}/enable") + @Operation(summary = "启用角色", description = "仅允许密钥管理员 FULL 会话启用目标角色。") + @RequireRole("KEY_ADMIN") + @RequireAuthLevel("FULL") + public ApiResponse enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) { + authAdminService.enableRole( + (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), + (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), + roleCode + ); + return ApiResponse.success(); + } + + @PostMapping("/roles/{roleCode}/reset-password") + @Operation(summary = "重置角色密码", description = "仅允许密钥管理员 FULL 会话重置目标角色密码。") + @RequireRole("KEY_ADMIN") + @RequireAuthLevel("FULL") + public ApiResponse resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) { + authAdminService.resetPassword( + (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), + (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), + roleCode + ); + return ApiResponse.success(); + } + + @PostMapping("/roles/{roleCode}/ukeys/bind") + @Operation(summary = "绑定角色 UKey", description = "仅允许密钥管理员 FULL 会话绑定或覆盖目标角色槽位上的 UKey。") + @RequireRole("KEY_ADMIN") + @RequireAuthLevel("FULL") + public ApiResponse bindUkey( + @PathVariable("roleCode") String roleCode, + @Valid @RequestBody UkeyBindRequest request, + HttpServletRequest httpRequest ) { - return ApiResponse.success(authService.me(user)); + authAdminService.bindIssuedUkey( + (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), + (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), + roleCode, + request.getSlotNo(), + request.getUkeySerial(), + request.getUkeyPubkey(), + request.getUid(), + request.getRid(), + request.getIssuerSign() + ); + return ApiResponse.success(); + } + + @PostMapping("/roles/{roleCode}/ukeys/issue-sign") + @Operation(summary = "生成绑定用 UKey 发行签名", description = "按旧管理端绑定流程为目标角色的 UKey 信息出具发行签名。") + @RequireRole("KEY_ADMIN") + @RequireAuthLevel("FULL") + public ApiResponse issueUkeyBindingSign( + @PathVariable("roleCode") String roleCode, + @RequestBody UKeySignDTO request, + HttpServletRequest httpRequest + ) { + return ApiResponse.success(authAdminService.issueUkeyBindingSign( + (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE), + (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL), + roleCode, + request + )); } } diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/CompatAuthController.java b/src/main/java/com/cisd/tms/modules/auth/controller/CompatAuthController.java new file mode 100644 index 0000000..5873cb2 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/controller/CompatAuthController.java @@ -0,0 +1,50 @@ +package com.cisd.tms.modules.auth.controller; + +import com.cisd.tms.common.api.ApiResponse; +import com.cisd.tms.modules.auth.dto.CompatPasswordLoginRequest; +import com.cisd.tms.modules.auth.dto.CompatUkeyLoginRequest; +import com.cisd.tms.modules.auth.dto.LoginResponse; +import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse; +import com.cisd.tms.modules.auth.service.CompatAuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/auth") +@Tag(name = "认证兼容接口", description = "兼容旧管理端的口令登录、UKey 登录与随机数接口") +public class CompatAuthController { + + private final CompatAuthService compatAuthService; + + public CompatAuthController(CompatAuthService compatAuthService) { + this.compatAuthService = compatAuthService; + } + + @GetMapping("/ukey-login/randoms") + @Operation(summary = "生成 UKey 登录随机数", description = "兼容旧登录流程,为指定角色生成一组待签名随机数。") + public ApiResponse issueUkeyLoginRandoms( + @RequestParam("role") String role, + @RequestParam(value = "count", required = false) Integer count + ) { + return ApiResponse.success(compatAuthService.issueUkeyLoginRandoms(role, count)); + } + + @PostMapping("/password-login") + @Operation(summary = "兼容口令登录", description = "兼容旧管理端的角色口令登录语义,底层复用当前会话体系。") + public ApiResponse passwordLogin(@Valid @RequestBody CompatPasswordLoginRequest request) { + return ApiResponse.success(compatAuthService.passwordLogin(request)); + } + + @PostMapping("/ukey-login") + @Operation(summary = "兼容 UKey 登录", description = "兼容旧管理端的 UKey 登录语义,校验发行签名、随机数和登录签名。") + public ApiResponse ukeyLogin(@Valid @RequestBody CompatUkeyLoginRequest request) { + return ApiResponse.success(compatAuthService.ukeyLogin(request)); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java new file mode 100644 index 0000000..d2f0ffa --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/ChangePasswordRequest.java @@ -0,0 +1,32 @@ +package com.cisd.tms.modules.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "修改当前角色口令请求") +public class ChangePasswordRequest { + + @NotBlank(message = "currentPassword is required") + @Schema(description = "当前口令", example = "12345678") + private String currentPassword; + + @NotBlank(message = "newPassword is required") + @Schema(description = "新口令", example = "87654321") + private String newPassword; + + public String getCurrentPassword() { + return currentPassword; + } + + public void setCurrentPassword(String currentPassword) { + this.currentPassword = currentPassword; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/CompatPasswordLoginRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/CompatPasswordLoginRequest.java new file mode 100644 index 0000000..471e91e --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/CompatPasswordLoginRequest.java @@ -0,0 +1,32 @@ +package com.cisd.tms.modules.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +@Schema(description = "兼容旧管理端的口令登录请求") +public class CompatPasswordLoginRequest { + + @NotBlank(message = "role is required") + @Schema(description = "角色标识,兼容旧角色名称", example = "auditadmin") + private String role; + + @NotBlank(message = "password is required") + @Schema(description = "登录口令", example = "12345678") + private String password; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/CompatUkeyLoginRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/CompatUkeyLoginRequest.java new file mode 100644 index 0000000..2b2a83c --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/CompatUkeyLoginRequest.java @@ -0,0 +1,132 @@ +package com.cisd.tms.modules.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import java.util.List; + +@Schema(description = "兼容旧管理端的 UKey 登录请求") +public class CompatUkeyLoginRequest { + + @NotBlank(message = "role is required") + @Schema(description = "角色标识,兼容旧角色名称", example = "keyadmin") + private String role; + + @NotBlank(message = "password is required") + @Schema(description = "登录口令", example = "12345678") + private String password; + + @Valid + @NotEmpty(message = "authInfo is required") + @Schema(description = "UKey 登录认证材料") + private List authInfo; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public List getAuthInfo() { + return authInfo; + } + + public void setAuthInfo(List authInfo) { + this.authInfo = authInfo; + } + + @Schema(description = "单个 UKey 登录认证项") + public static class LoginAuthInfo { + @NotBlank(message = "pubKey is required") + private String pubKey; + @NotBlank(message = "uid is required") + private String uid; + @NotBlank(message = "rid is required") + private String rid; + @NotBlank(message = "ra is required") + private String ra; + @NotBlank(message = "rb is required") + private String rb; + @NotBlank(message = "issueSign is required") + private String issueSign; + @NotBlank(message = "loginSignData is required") + private String loginSignData; + @NotBlank(message = "loginSign is required") + private String loginSign; + + public String getPubKey() { + return pubKey; + } + + public void setPubKey(String pubKey) { + this.pubKey = pubKey; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public String getRid() { + return rid; + } + + public void setRid(String rid) { + this.rid = rid; + } + + public String getRa() { + return ra; + } + + public void setRa(String ra) { + this.ra = ra; + } + + public String getRb() { + return rb; + } + + public void setRb(String rb) { + this.rb = rb; + } + + public String getIssueSign() { + return issueSign; + } + + public void setIssueSign(String issueSign) { + this.issueSign = issueSign; + } + + public String getLoginSignData() { + return loginSignData; + } + + public void setLoginSignData(String loginSignData) { + this.loginSignData = loginSignData; + } + + public String getLoginSign() { + return loginSign; + } + + public void setLoginSign(String loginSign) { + this.loginSign = loginSign; + } + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/CurrentUserResponse.java b/src/main/java/com/cisd/tms/modules/auth/dto/CurrentUserResponse.java index 3fa14e4..ef33fec 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/CurrentUserResponse.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/CurrentUserResponse.java @@ -11,6 +11,10 @@ public class CurrentUserResponse { private String displayName; @Schema(description = "角色标识", example = "ADMIN") private String role; + @Schema(description = "当前会话等级", example = "FULL") + private String authLevel; + @Schema(description = "是否需要强制修改密码") + private Boolean needChangePassword; public String getUsername() { return username; @@ -35,4 +39,20 @@ public class CurrentUserResponse { public void setRole(String role) { this.role = role; } + + public String getAuthLevel() { + return authLevel; + } + + public void setAuthLevel(String authLevel) { + this.authLevel = authLevel; + } + + public Boolean getNeedChangePassword() { + return needChangePassword; + } + + public void setNeedChangePassword(Boolean needChangePassword) { + this.needChangePassword = needChangePassword; + } } diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/LoginRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/LoginRequest.java index fcd0b4e..fb73799 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/LoginRequest.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/LoginRequest.java @@ -2,24 +2,28 @@ package com.cisd.tms.modules.auth.dto; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import java.util.List; @Schema(description = "内部登录请求") public class LoginRequest { - @NotBlank(message = "username is required") - @Schema(description = "登录用户名", example = "admin") - private String username; + @NotBlank(message = "roleCode is required") + @Schema(description = "角色编码", example = "KEY_ADMIN") + private String roleCode; @NotBlank(message = "password is required") @Schema(description = "登录密码", example = "P@ssw0rd") private String password; - public String getUsername() { - return username; + @Schema(description = "UKey 序列号列表,完整登录时需传入角色要求数量的序列号") + private List<@NotBlank(message = "ukey serial must not be blank") String> ukeySerials; + + public String getRoleCode() { + return roleCode; } - public void setUsername(String username) { - this.username = username; + public void setRoleCode(String roleCode) { + this.roleCode = roleCode; } public String getPassword() { @@ -29,4 +33,12 @@ public class LoginRequest { public void setPassword(String password) { this.password = password; } + + public List getUkeySerials() { + return ukeySerials; + } + + public void setUkeySerials(List ukeySerials) { + this.ukeySerials = ukeySerials; + } } diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/LoginResponse.java b/src/main/java/com/cisd/tms/modules/auth/dto/LoginResponse.java index b5873f0..f674e75 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/LoginResponse.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/LoginResponse.java @@ -5,19 +5,23 @@ import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "内部登录响应") public class LoginResponse { - @Schema(description = "登录用户名", example = "admin") - private String username; + @Schema(description = "角色编码", example = "KEY_ADMIN") + private String roleCode; @Schema(description = "访问令牌") private String token; + @Schema(description = "认证等级", example = "FULL") + private String authLevel; @Schema(description = "令牌过期时间", example = "2026-03-11T18:00:00+08:00") private String expiresAt; + @Schema(description = "是否需要首次修改密码") + private Boolean needChangePassword; - public String getUsername() { - return username; + public String getRoleCode() { + return roleCode; } - public void setUsername(String username) { - this.username = username; + public void setRoleCode(String roleCode) { + this.roleCode = roleCode; } public String getToken() { @@ -28,6 +32,14 @@ public class LoginResponse { this.token = token; } + public String getAuthLevel() { + return authLevel; + } + + public void setAuthLevel(String authLevel) { + this.authLevel = authLevel; + } + public String getExpiresAt() { return expiresAt; } @@ -35,4 +47,12 @@ public class LoginResponse { public void setExpiresAt(String expiresAt) { this.expiresAt = expiresAt; } + + public Boolean getNeedChangePassword() { + return needChangePassword; + } + + public void setNeedChangePassword(Boolean needChangePassword) { + this.needChangePassword = needChangePassword; + } } diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/UkeyBindRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyBindRequest.java new file mode 100644 index 0000000..eaec17d --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyBindRequest.java @@ -0,0 +1,81 @@ +package com.cisd.tms.modules.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "UKey 绑定请求") +public class UkeyBindRequest { + + @NotNull(message = "slotNo is required") + @Schema(description = "角色 UKey 槽位号", example = "1") + private Integer slotNo; + + @NotBlank(message = "ukeySerial is required") + @Schema(description = "UKey 序列号", example = "UK-001") + private String ukeySerial; + + @NotBlank(message = "ukeyPubkey is required") + @Schema(description = "UKey 公钥") + private String ukeyPubkey; + + @NotBlank(message = "uid is required") + @Schema(description = "UKey 认证信息 uid", example = "1") + private String uid; + + @NotBlank(message = "rid is required") + @Schema(description = "UKey 认证信息 rid", example = "RID-001") + private String rid; + + @NotBlank(message = "issuerSign is required") + @Schema(description = "认证公钥签名值") + private String issuerSign; + + public Integer getSlotNo() { + return slotNo; + } + + public void setSlotNo(Integer slotNo) { + this.slotNo = slotNo; + } + + public String getUkeySerial() { + return ukeySerial; + } + + public void setUkeySerial(String ukeySerial) { + this.ukeySerial = ukeySerial; + } + + public String getUkeyPubkey() { + return ukeyPubkey; + } + + public void setUkeyPubkey(String ukeyPubkey) { + this.ukeyPubkey = ukeyPubkey; + } + + public String getIssuerSign() { + return issuerSign; + } + + public void setIssuerSign(String issuerSign) { + this.issuerSign = issuerSign; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public String getRid() { + return rid; + } + + public void setRid(String rid) { + this.rid = rid; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginRandomResponse.java b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginRandomResponse.java new file mode 100644 index 0000000..88f346a --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginRandomResponse.java @@ -0,0 +1,30 @@ +package com.cisd.tms.modules.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +@Schema(description = "UKey 登录随机数响应") +public class UkeyLoginRandomResponse { + + @Schema(description = "标准化后的角色编码", example = "SUPER_ADMIN") + private String roleCode; + + @Schema(description = "供 UKey 登录签名使用的随机数列表") + private List randoms; + + public String getRoleCode() { + return roleCode; + } + + public void setRoleCode(String roleCode) { + this.roleCode = roleCode; + } + + public List getRandoms() { + return randoms; + } + + public void setRandoms(List randoms) { + this.randoms = randoms; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/entity/AuthSessionEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/AuthSessionEntity.java new file mode 100644 index 0000000..f72eab3 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/entity/AuthSessionEntity.java @@ -0,0 +1,73 @@ +package com.cisd.tms.modules.auth.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.cisd.tms.infrastructure.persistence.entity.BaseEntity; +import java.time.LocalDateTime; + +@TableName("tms_auth_session") +public class AuthSessionEntity extends BaseEntity { + + private String sessionToken; + private String roleCode; + private String authLevel; + private LocalDateTime issuedAt; + private LocalDateTime lastActiveAt; + private LocalDateTime expiresAt; + private LocalDateTime logoutAt; + + public String getSessionToken() { + return sessionToken; + } + + public void setSessionToken(String sessionToken) { + this.sessionToken = sessionToken; + } + + public String getRoleCode() { + return roleCode; + } + + public void setRoleCode(String roleCode) { + this.roleCode = roleCode; + } + + public String getAuthLevel() { + return authLevel; + } + + public void setAuthLevel(String authLevel) { + this.authLevel = authLevel; + } + + public LocalDateTime getIssuedAt() { + return issuedAt; + } + + public void setIssuedAt(LocalDateTime issuedAt) { + this.issuedAt = issuedAt; + } + + public LocalDateTime getLastActiveAt() { + return lastActiveAt; + } + + public void setLastActiveAt(LocalDateTime lastActiveAt) { + this.lastActiveAt = lastActiveAt; + } + + public LocalDateTime getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(LocalDateTime expiresAt) { + this.expiresAt = expiresAt; + } + + public LocalDateTime getLogoutAt() { + return logoutAt; + } + + public void setLogoutAt(LocalDateTime logoutAt) { + this.logoutAt = logoutAt; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/entity/RoleAccountEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/RoleAccountEntity.java new file mode 100644 index 0000000..53b07ff --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/entity/RoleAccountEntity.java @@ -0,0 +1,109 @@ +package com.cisd.tms.modules.auth.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.cisd.tms.infrastructure.persistence.entity.BaseEntity; +import java.time.LocalDateTime; + +@TableName("tms_role_account") +public class RoleAccountEntity extends BaseEntity { + + private String roleCode; + private String displayName; + private Integer requiredUkeyCount; + private String passwordHash; + private String passwordSalt; + private String status; + private Boolean needChangePassword; + private Integer failedCount; + private LocalDateTime lockedUntil; + private LocalDateTime lastLoginAt; + private LocalDateTime lastActiveAt; + + public String getRoleCode() { + return roleCode; + } + + public void setRoleCode(String roleCode) { + this.roleCode = roleCode; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public Integer getRequiredUkeyCount() { + return requiredUkeyCount; + } + + public void setRequiredUkeyCount(Integer requiredUkeyCount) { + this.requiredUkeyCount = requiredUkeyCount; + } + + public String getPasswordHash() { + return passwordHash; + } + + public void setPasswordHash(String passwordHash) { + this.passwordHash = passwordHash; + } + + public String getPasswordSalt() { + return passwordSalt; + } + + public void setPasswordSalt(String passwordSalt) { + this.passwordSalt = passwordSalt; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Boolean getNeedChangePassword() { + return needChangePassword; + } + + public void setNeedChangePassword(Boolean needChangePassword) { + this.needChangePassword = needChangePassword; + } + + public Integer getFailedCount() { + return failedCount; + } + + public void setFailedCount(Integer failedCount) { + this.failedCount = failedCount; + } + + public LocalDateTime getLockedUntil() { + return lockedUntil; + } + + public void setLockedUntil(LocalDateTime lockedUntil) { + this.lockedUntil = lockedUntil; + } + + public LocalDateTime getLastLoginAt() { + return lastLoginAt; + } + + public void setLastLoginAt(LocalDateTime lastLoginAt) { + this.lastLoginAt = lastLoginAt; + } + + public LocalDateTime getLastActiveAt() { + return lastActiveAt; + } + + public void setLastActiveAt(LocalDateTime lastActiveAt) { + this.lastActiveAt = lastActiveAt; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/entity/RoleUkeyBindingEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/RoleUkeyBindingEntity.java new file mode 100644 index 0000000..0f12476 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/entity/RoleUkeyBindingEntity.java @@ -0,0 +1,82 @@ +package com.cisd.tms.modules.auth.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.cisd.tms.infrastructure.persistence.entity.BaseEntity; +import java.time.LocalDateTime; + +@TableName("tms_role_ukey_binding") +public class RoleUkeyBindingEntity extends BaseEntity { + + private String roleCode; + private Integer slotNo; + private String ukeySerial; + private String ukeyPubkey; + private String issuerSign; + private String status; + private LocalDateTime boundAt; + private LocalDateTime unboundAt; + + public String getRoleCode() { + return roleCode; + } + + public void setRoleCode(String roleCode) { + this.roleCode = roleCode; + } + + public Integer getSlotNo() { + return slotNo; + } + + public void setSlotNo(Integer slotNo) { + this.slotNo = slotNo; + } + + public String getUkeySerial() { + return ukeySerial; + } + + public void setUkeySerial(String ukeySerial) { + this.ukeySerial = ukeySerial; + } + + public String getUkeyPubkey() { + return ukeyPubkey; + } + + public void setUkeyPubkey(String ukeyPubkey) { + this.ukeyPubkey = ukeyPubkey; + } + + public String getIssuerSign() { + return issuerSign; + } + + public void setIssuerSign(String issuerSign) { + this.issuerSign = issuerSign; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public LocalDateTime getBoundAt() { + return boundAt; + } + + public void setBoundAt(LocalDateTime boundAt) { + this.boundAt = boundAt; + } + + public LocalDateTime getUnboundAt() { + return unboundAt; + } + + public void setUnboundAt(LocalDateTime unboundAt) { + this.unboundAt = unboundAt; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/enums/AuthLevel.java b/src/main/java/com/cisd/tms/modules/auth/enums/AuthLevel.java new file mode 100644 index 0000000..d241d5d --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/enums/AuthLevel.java @@ -0,0 +1,6 @@ +package com.cisd.tms.modules.auth.enums; + +public enum AuthLevel { + LIMITED, + FULL +} diff --git a/src/main/java/com/cisd/tms/modules/auth/enums/RoleAccountStatus.java b/src/main/java/com/cisd/tms/modules/auth/enums/RoleAccountStatus.java new file mode 100644 index 0000000..88c9c66 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/enums/RoleAccountStatus.java @@ -0,0 +1,7 @@ +package com.cisd.tms.modules.auth.enums; + +public enum RoleAccountStatus { + UNENABLED, + ACTIVE, + LOCKED +} diff --git a/src/main/java/com/cisd/tms/modules/auth/enums/RoleCode.java b/src/main/java/com/cisd/tms/modules/auth/enums/RoleCode.java new file mode 100644 index 0000000..6b753cd --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/enums/RoleCode.java @@ -0,0 +1,30 @@ +package com.cisd.tms.modules.auth.enums; + +public enum RoleCode { + SUPER_ADMIN("SUPER_ADMIN", "超级管理员", 3), + KEY_ADMIN("KEY_ADMIN", "密钥管理员", 2), + AUDIT_ADMIN("AUDIT_ADMIN", "审计管理员", 1), + OPS_ADMIN("OPS_ADMIN", "运维管理员", 1); + + private final String code; + private final String displayName; + private final int requiredUkeyCount; + + RoleCode(String code, String displayName, int requiredUkeyCount) { + this.code = code; + this.displayName = displayName; + this.requiredUkeyCount = requiredUkeyCount; + } + + public String getCode() { + return code; + } + + public String getDisplayName() { + return displayName; + } + + public int getRequiredUkeyCount() { + return requiredUkeyCount; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java new file mode 100644 index 0000000..c74de70 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/mapper/AuthSessionMapper.java @@ -0,0 +1,12 @@ +package com.cisd.tms.modules.auth.mapper; + +import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX; +import com.cisd.tms.modules.auth.entity.AuthSessionEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface AuthSessionMapper extends BaseMapperX { + + AuthSessionEntity selectBySessionToken(@Param("sessionToken") String sessionToken); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/mapper/RoleAccountMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/RoleAccountMapper.java new file mode 100644 index 0000000..2d41fb7 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/mapper/RoleAccountMapper.java @@ -0,0 +1,12 @@ +package com.cisd.tms.modules.auth.mapper; + +import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX; +import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface RoleAccountMapper extends BaseMapperX { + + RoleAccountEntity selectByRoleCode(@Param("roleCode") String roleCode); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/mapper/RoleUkeyBindingMapper.java b/src/main/java/com/cisd/tms/modules/auth/mapper/RoleUkeyBindingMapper.java new file mode 100644 index 0000000..df18620 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/mapper/RoleUkeyBindingMapper.java @@ -0,0 +1,15 @@ +package com.cisd.tms.modules.auth.mapper; + +import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX; +import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface RoleUkeyBindingMapper extends BaseMapperX { + + RoleUkeyBindingEntity selectActiveByRoleCodeAndSlotNo(@Param("roleCode") String roleCode, @Param("slotNo") Integer slotNo); + + List selectActiveByRoleCode(@Param("roleCode") String roleCode); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java new file mode 100644 index 0000000..a9bc12d --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/repository/AuthSessionRepository.java @@ -0,0 +1,13 @@ +package com.cisd.tms.modules.auth.repository; + +import com.cisd.tms.modules.auth.entity.AuthSessionEntity; +import java.util.Optional; + +public interface AuthSessionRepository { + + Optional findBySessionToken(String sessionToken); + + void save(AuthSessionEntity entity); + + void update(AuthSessionEntity entity); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/RoleAccountRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/RoleAccountRepository.java new file mode 100644 index 0000000..dc57254 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/repository/RoleAccountRepository.java @@ -0,0 +1,13 @@ +package com.cisd.tms.modules.auth.repository; + +import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import java.util.Optional; + +public interface RoleAccountRepository { + + Optional findByRoleCode(String roleCode); + + void save(RoleAccountEntity entity); + + void update(RoleAccountEntity entity); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/RoleUkeyBindingRepository.java b/src/main/java/com/cisd/tms/modules/auth/repository/RoleUkeyBindingRepository.java new file mode 100644 index 0000000..9d50186 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/repository/RoleUkeyBindingRepository.java @@ -0,0 +1,16 @@ +package com.cisd.tms.modules.auth.repository; + +import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; +import java.util.List; +import java.util.Optional; + +public interface RoleUkeyBindingRepository { + + Optional findActiveByRoleCodeAndSlotNo(String roleCode, Integer slotNo); + + List findActiveByRoleCode(String roleCode); + + void save(RoleUkeyBindingEntity entity); + + void update(RoleUkeyBindingEntity entity); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java new file mode 100644 index 0000000..f9ee51f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/repository/impl/AuthSessionRepositoryImpl.java @@ -0,0 +1,32 @@ +package com.cisd.tms.modules.auth.repository.impl; + +import com.cisd.tms.modules.auth.entity.AuthSessionEntity; +import com.cisd.tms.modules.auth.mapper.AuthSessionMapper; +import com.cisd.tms.modules.auth.repository.AuthSessionRepository; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public class AuthSessionRepositoryImpl implements AuthSessionRepository { + + private final AuthSessionMapper authSessionMapper; + + public AuthSessionRepositoryImpl(AuthSessionMapper authSessionMapper) { + this.authSessionMapper = authSessionMapper; + } + + @Override + public Optional findBySessionToken(String sessionToken) { + return Optional.ofNullable(authSessionMapper.selectBySessionToken(sessionToken)); + } + + @Override + public void save(AuthSessionEntity entity) { + authSessionMapper.insert(entity); + } + + @Override + public void update(AuthSessionEntity entity) { + authSessionMapper.updateById(entity); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleAccountRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleAccountRepositoryImpl.java new file mode 100644 index 0000000..eb277b2 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleAccountRepositoryImpl.java @@ -0,0 +1,32 @@ +package com.cisd.tms.modules.auth.repository.impl; + +import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import com.cisd.tms.modules.auth.mapper.RoleAccountMapper; +import com.cisd.tms.modules.auth.repository.RoleAccountRepository; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public class RoleAccountRepositoryImpl implements RoleAccountRepository { + + private final RoleAccountMapper roleAccountMapper; + + public RoleAccountRepositoryImpl(RoleAccountMapper roleAccountMapper) { + this.roleAccountMapper = roleAccountMapper; + } + + @Override + public Optional findByRoleCode(String roleCode) { + return Optional.ofNullable(roleAccountMapper.selectByRoleCode(roleCode)); + } + + @Override + public void save(RoleAccountEntity entity) { + roleAccountMapper.insert(entity); + } + + @Override + public void update(RoleAccountEntity entity) { + roleAccountMapper.updateById(entity); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleUkeyBindingRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleUkeyBindingRepositoryImpl.java new file mode 100644 index 0000000..1171039 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/repository/impl/RoleUkeyBindingRepositoryImpl.java @@ -0,0 +1,38 @@ +package com.cisd.tms.modules.auth.repository.impl; + +import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; +import com.cisd.tms.modules.auth.mapper.RoleUkeyBindingMapper; +import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; +import java.util.List; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository { + + private final RoleUkeyBindingMapper roleUkeyBindingMapper; + + public RoleUkeyBindingRepositoryImpl(RoleUkeyBindingMapper roleUkeyBindingMapper) { + this.roleUkeyBindingMapper = roleUkeyBindingMapper; + } + + @Override + public Optional findActiveByRoleCodeAndSlotNo(String roleCode, Integer slotNo) { + return Optional.ofNullable(roleUkeyBindingMapper.selectActiveByRoleCodeAndSlotNo(roleCode, slotNo)); + } + + @Override + public List findActiveByRoleCode(String roleCode) { + return roleUkeyBindingMapper.selectActiveByRoleCode(roleCode); + } + + @Override + public void save(RoleUkeyBindingEntity entity) { + roleUkeyBindingMapper.insert(entity); + } + + @Override + public void update(RoleUkeyBindingEntity entity) { + roleUkeyBindingMapper.updateById(entity); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java b/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java new file mode 100644 index 0000000..388fd23 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptor.java @@ -0,0 +1,68 @@ +package com.cisd.tms.modules.auth.security; + +import com.cisd.tms.common.api.ApiResponse; +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.util.HttpResponseUtil; +import com.cisd.tms.security.internal.InternalApiAuthInterceptor; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class InternalAuthorizationInterceptor implements HandlerInterceptor { + + private final ObjectMapper objectMapper; + + public InternalAuthorizationInterceptor(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (!(handler instanceof HandlerMethod handlerMethod)) { + return true; + } + + RequireRole requireRole = findAnnotation(handlerMethod, RequireRole.class); + if (requireRole != null) { + String currentRole = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE); + if (!requireRole.value().equals(currentRole)) { + writeForbidden(response, "role not allowed"); + return false; + } + } + + RequireAuthLevel requireAuthLevel = findAnnotation(handlerMethod, RequireAuthLevel.class); + if (requireAuthLevel != null) { + String currentAuthLevel = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL); + if (!requireAuthLevel.value().equals(currentAuthLevel)) { + writeForbidden(response, "auth level not allowed"); + return false; + } + } + + return true; + } + + private T findAnnotation(HandlerMethod handlerMethod, Class annotationType) { + T methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getMethod(), annotationType); + if (methodAnnotation != null) { + return methodAnnotation; + } + return AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), annotationType); + } + + private void writeForbidden(HttpServletResponse response, String message) throws IOException { + HttpResponseUtil.writeJson( + response, + HttpServletResponse.SC_FORBIDDEN, + ApiResponse.fail(ErrorCode.UNAUTHORIZED.getCode(), message), + objectMapper + ); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/security/RequireAuthLevel.java b/src/main/java/com/cisd/tms/modules/auth/security/RequireAuthLevel.java new file mode 100644 index 0000000..91b9085 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/security/RequireAuthLevel.java @@ -0,0 +1,13 @@ +package com.cisd.tms.modules.auth.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequireAuthLevel { + + String value(); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/security/RequireRole.java b/src/main/java/com/cisd/tms/modules/auth/security/RequireRole.java new file mode 100644 index 0000000..aa66c4d --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/security/RequireRole.java @@ -0,0 +1,13 @@ +package com.cisd.tms.modules.auth.security; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface RequireRole { + + String value(); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java new file mode 100644 index 0000000..492c31e --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java @@ -0,0 +1,276 @@ +package com.cisd.tms.modules.auth.service; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.integration.crypto.pcie.service.MockPcieCryptoService; +import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; +import com.cisd.tms.modules.auth.enums.AuthLevel; +import com.cisd.tms.modules.auth.enums.RoleAccountStatus; +import com.cisd.tms.modules.auth.enums.RoleCode; +import com.cisd.tms.modules.auth.repository.RoleAccountRepository; +import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; +import com.cisd.tms.common.util.TraceIdUtil; +import com.cisd.tms.modules.mk.config.MasterKeyInitProperties; +import com.cisd.tms.modules.mk.dto.UKeySignDTO; +import com.cisd.tms.modules.mk.dto.UKeySignEntity; +import com.cisd.tms.modules.mk.dto.UKeySignResult; +import com.cisd.tms.modules.mk.service.LmkService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Supplier; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class AuthAdminService { + + static final String DEFAULT_PASSWORD = "12345678"; + + private final RoleAccountRepository roleAccountRepository; + private final RoleUkeyBindingRepository roleUkeyBindingRepository; + private final PasswordHasher passwordHasher; + private final LmkService lmkService; + private final ObjectMapper objectMapper; + private final Clock clock; + private final Supplier saltSupplier; + + @Autowired + public AuthAdminService( + RoleAccountRepository roleAccountRepository, + RoleUkeyBindingRepository roleUkeyBindingRepository, + PasswordHasher passwordHasher, + LmkService lmkService, + ObjectMapper objectMapper + ) { + this( + roleAccountRepository, + roleUkeyBindingRepository, + passwordHasher, + lmkService, + objectMapper, + Clock.systemUTC(), + () -> UUID.randomUUID().toString().replace("-", "") + ); + } + + AuthAdminService( + RoleAccountRepository roleAccountRepository, + RoleUkeyBindingRepository roleUkeyBindingRepository, + LmkService lmkService, + PasswordHasher passwordHasher, + Clock clock, + Supplier saltSupplier + ) { + this( + roleAccountRepository, + roleUkeyBindingRepository, + passwordHasher, + lmkService, + new ObjectMapper(), + clock, + saltSupplier + ); + } + + AuthAdminService( + RoleAccountRepository roleAccountRepository, + RoleUkeyBindingRepository roleUkeyBindingRepository, + PasswordHasher passwordHasher, + Clock clock, + Supplier saltSupplier + ) { + this( + roleAccountRepository, + roleUkeyBindingRepository, + new LmkService(new MockPcieCryptoService(), new MasterKeyInitProperties()), + passwordHasher, + clock, + saltSupplier + ); + } + + AuthAdminService( + RoleAccountRepository roleAccountRepository, + RoleUkeyBindingRepository roleUkeyBindingRepository, + PasswordHasher passwordHasher, + LmkService lmkService, + ObjectMapper objectMapper, + Clock clock, + Supplier saltSupplier + ) { + this.roleAccountRepository = roleAccountRepository; + this.roleUkeyBindingRepository = roleUkeyBindingRepository; + this.passwordHasher = passwordHasher; + this.lmkService = lmkService; + this.objectMapper = objectMapper; + this.clock = clock; + this.saltSupplier = saltSupplier; + } + + public void enableRole(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) { + requireKeyAdminFull(operatorRoleCode, operatorAuthLevel); + RoleAccountEntity target = loadRole(targetRoleCode); + target.setStatus(RoleAccountStatus.ACTIVE.name()); + target.setNeedChangePassword(Boolean.TRUE); + target.setLockedUntil(null); + target.setFailedCount(0); + roleAccountRepository.update(target); + } + + public void resetPassword(String operatorRoleCode, String operatorAuthLevel, String targetRoleCode) { + requireKeyAdminFull(operatorRoleCode, operatorAuthLevel); + RoleAccountEntity target = loadRole(targetRoleCode); + String newSalt = saltSupplier.get(); + target.setPasswordSalt(newSalt); + target.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD, newSalt)); + target.setStatus(RoleAccountStatus.ACTIVE.name()); + target.setNeedChangePassword(Boolean.TRUE); + target.setFailedCount(0); + target.setLockedUntil(null); + roleAccountRepository.update(target); + } + + public void bindUkey( + String operatorRoleCode, + String operatorAuthLevel, + String targetRoleCode, + Integer slotNo, + String ukeySerial, + String ukeyPubkey, + String issuerSign + ) { + requireKeyAdminFull(operatorRoleCode, operatorAuthLevel); + RoleCode targetRole = resolveRoleCode(targetRoleCode); + if (slotNo == null || slotNo < 1 || slotNo > targetRole.getRequiredUkeyCount()) { + throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "slotNo exceeds role ukey requirement"); + } + + RoleUkeyBindingEntity binding = roleUkeyBindingRepository + .findActiveByRoleCodeAndSlotNo(targetRoleCode, slotNo) + .orElseGet(RoleUkeyBindingEntity::new); + if (binding.getId() == null) { + binding.setId((long) Math.abs(Objects.hash(targetRoleCode, slotNo, ukeySerial, TraceIdUtil.newTraceId()))); + binding.setRoleCode(targetRoleCode); + binding.setSlotNo(slotNo); + binding.setStatus("ACTIVE"); + applyBinding(binding, ukeySerial, ukeyPubkey, issuerSign); + roleUkeyBindingRepository.save(binding); + return; + } + + applyBinding(binding, ukeySerial, ukeyPubkey, issuerSign); + roleUkeyBindingRepository.update(binding); + } + + public void bindIssuedUkey( + String operatorRoleCode, + String operatorAuthLevel, + String targetRoleCode, + Integer slotNo, + String ukeySerial, + String ukeyPubkey, + String uid, + String rid, + String issuerSign + ) { + requireKeyAdminFull(operatorRoleCode, operatorAuthLevel); + resolveRoleCode(targetRoleCode); + verifyIssuerSignature(targetRoleCode, ukeyPubkey, uid, rid, issuerSign); + bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, slotNo, ukeySerial, ukeyPubkey, issuerSign); + } + + public UKeySignResult issueUkeyBindingSign( + String operatorRoleCode, + String operatorAuthLevel, + String targetRoleCode, + UKeySignDTO request + ) { + requireKeyAdminFull(operatorRoleCode, operatorAuthLevel); + RoleCode targetRole = resolveRoleCode(targetRoleCode); + String legacyRole = toLegacyRole(targetRole); + String authKeyPair = lmkService.exportIkPublicKeyHex(); + + UKeySignDTO dto = new UKeySignDTO(); + dto.setPubKey(request.getPubKey()); + dto.setRole(legacyRole); + dto.setUid(request.getUid()); + dto.setRid(request.getRid()); + dto.setExtra(request.getExtra()); + + String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair)); + String extra = ""; + return UKeySignResult.builder().sign(signValue).component(extra).build(); + } + + private void applyBinding(RoleUkeyBindingEntity binding, String ukeySerial, String ukeyPubkey, String issuerSign) { + binding.setUkeySerial(ukeySerial); + binding.setUkeyPubkey(ukeyPubkey); + binding.setIssuerSign(issuerSign); + binding.setStatus("ACTIVE"); + binding.setBoundAt(now()); + binding.setUnboundAt(null); + } + + private void requireKeyAdminFull(String operatorRoleCode, String operatorAuthLevel) { + if (!RoleCode.KEY_ADMIN.getCode().equals(operatorRoleCode) || !AuthLevel.FULL.name().equals(operatorAuthLevel)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "operation requires KEY_ADMIN FULL session"); + } + } + + private RoleAccountEntity loadRole(String roleCode) { + resolveRoleCode(roleCode); + return roleAccountRepository.findByRoleCode(roleCode) + .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role account not found")); + } + + private RoleCode resolveRoleCode(String roleCode) { + return List.of(RoleCode.values()).stream() + .filter(item -> item.getCode().equals(roleCode)) + .findFirst() + .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "invalid roleCode")); + } + + private LocalDateTime now() { + return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC); + } + + private void verifyIssuerSignature(String targetRoleCode, String ukeyPubkey, String uid, String rid, String issuerSign) { + try { + UKeySignDTO dto = new UKeySignDTO(); + dto.setPubKey(ukeyPubkey); + dto.setRole(toLegacyRole(resolveRoleCode(targetRoleCode))); + dto.setUid(uid); + dto.setRid(rid); + String authKeyPair = lmkService.exportIkPublicKeyHex(); + lmkService.verifyIk(toIssuePayload(dto, authKeyPair), issuerSign); + } catch (BizException ex) { + throw ex; + } catch (RuntimeException ex) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey issuer signature verification failed"); + } + } + + private String toIssuePayload(UKeySignDTO dto, String authKeyPair) { + try { + return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair)); + } catch (JsonProcessingException ex) { + throw new IllegalStateException("serialize ukey issue payload failed", ex); + } + } + + private String toLegacyRole(RoleCode roleCode) { + return switch (roleCode) { + case SUPER_ADMIN -> "superadmin"; + case KEY_ADMIN -> "keyadmin"; + case AUDIT_ADMIN -> "auditadmin"; + case OPS_ADMIN -> "configadmin"; + }; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java index 32ae517..2de2c96 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java @@ -1,33 +1,126 @@ package com.cisd.tms.modules.auth.service; +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; import com.cisd.tms.modules.auth.dto.CurrentUserResponse; import com.cisd.tms.modules.auth.dto.LoginRequest; import com.cisd.tms.modules.auth.dto.LoginResponse; +import com.cisd.tms.modules.auth.entity.AuthSessionEntity; import com.cisd.tms.modules.auth.entity.AuthUserEntity; +import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; +import com.cisd.tms.modules.auth.enums.AuthLevel; +import com.cisd.tms.modules.auth.enums.RoleAccountStatus; import com.cisd.tms.modules.auth.repository.AuthUserRepository; +import com.cisd.tms.modules.auth.repository.AuthSessionRepository; +import com.cisd.tms.modules.auth.repository.RoleAccountRepository; +import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; +import java.time.Clock; +import java.time.LocalDateTime; import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; import java.util.UUID; +import java.util.function.Supplier; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class AuthService { - private final AuthUserRepository authUserRepository; + private static final int MAX_FAILED_ATTEMPTS = 5; + private static final int IDLE_TIMEOUT_MINUTES = 10; - public AuthService(AuthUserRepository authUserRepository) { + private final RoleAccountRepository roleAccountRepository; + private final AuthUserRepository authUserRepository; + private final AuthSessionRepository authSessionRepository; + private final RoleUkeyBindingRepository roleUkeyBindingRepository; + private final PasswordHasher passwordHasher; + private final Clock clock; + private final Supplier tokenSupplier; + + @Autowired + public AuthService( + RoleAccountRepository roleAccountRepository, + AuthUserRepository authUserRepository, + AuthSessionRepository authSessionRepository, + RoleUkeyBindingRepository roleUkeyBindingRepository, + PasswordHasher passwordHasher + ) { + this( + roleAccountRepository, + authUserRepository, + authSessionRepository, + roleUkeyBindingRepository, + passwordHasher, + Clock.systemUTC(), + () -> "tms-" + UUID.randomUUID() + ); + } + + AuthService( + RoleAccountRepository roleAccountRepository, + AuthUserRepository authUserRepository, + AuthSessionRepository authSessionRepository, + RoleUkeyBindingRepository roleUkeyBindingRepository, + PasswordHasher passwordHasher, + Clock clock, + Supplier tokenSupplier + ) { + this.roleAccountRepository = roleAccountRepository; this.authUserRepository = authUserRepository; + this.authSessionRepository = authSessionRepository; + this.roleUkeyBindingRepository = roleUkeyBindingRepository; + this.passwordHasher = passwordHasher; + this.clock = clock; + this.tokenSupplier = tokenSupplier; } public LoginResponse login(LoginRequest request) { + RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(request.getRoleCode()) + .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found")); + + validateRoleStatus(roleAccount); + if (!passwordHasher.matches(request.getPassword(), roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) { + onPasswordFailed(roleAccount); + } + + resetFailureState(roleAccount); + AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials()); + AuthSessionEntity session = buildSession(roleAccount.getRoleCode(), authLevel); + authSessionRepository.save(session); + LoginResponse response = new LoginResponse(); - response.setUsername(request.getUsername()); - response.setToken("tms-" + UUID.randomUUID()); - response.setExpiresAt(OffsetDateTime.now().plusHours(8).toString()); + response.setRoleCode(roleAccount.getRoleCode()); + response.setAuthLevel(authLevel.name()); + response.setToken(session.getSessionToken()); + response.setExpiresAt(OffsetDateTime.of(session.getExpiresAt(), ZoneOffset.UTC).toString()); + response.setNeedChangePassword(Boolean.TRUE.equals(roleAccount.getNeedChangePassword())); return response; } public CurrentUserResponse me(String username) { + return me(username, null); + } + + public CurrentUserResponse me(String username, String sessionToken) { String normalized = username == null || username.isBlank() ? "admin" : username; + RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(normalized).orElse(null); + if (roleAccount != null) { + CurrentUserResponse response = new CurrentUserResponse(); + response.setUsername(roleAccount.getRoleCode()); + response.setDisplayName(roleAccount.getDisplayName()); + response.setRole(roleAccount.getRoleCode()); + AuthSessionEntity session = sessionToken == null ? null : authSessionRepository.findBySessionToken(sessionToken).orElse(null); + if (session != null) { + response.setAuthLevel(session.getAuthLevel()); + } + response.setNeedChangePassword(Boolean.TRUE.equals(roleAccount.getNeedChangePassword())); + return response; + } + AuthUserEntity entity = authUserRepository.findByUsername(normalized).orElse(null); CurrentUserResponse response = new CurrentUserResponse(); @@ -43,4 +136,114 @@ public class AuthService { response.setRole(entity.getRole()); return response; } + + public void logout(String sessionToken) { + AuthSessionEntity session = requireActiveSession(sessionToken); + session.setLogoutAt(now()); + session.setExpiresAt(now()); + authSessionRepository.update(session); + } + + public void changePassword(String sessionToken, String currentPassword, String newPassword) { + AuthSessionEntity session = requireActiveSession(sessionToken); + RoleAccountEntity roleAccount = roleAccountRepository.findByRoleCode(session.getRoleCode()) + .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account not found")); + if (!passwordHasher.matches(currentPassword, roleAccount.getPasswordSalt(), roleAccount.getPasswordHash())) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "current password is incorrect"); + } + + String newSalt = UUID.randomUUID().toString().replace("-", ""); + roleAccount.setPasswordSalt(newSalt); + roleAccount.setPasswordHash(passwordHasher.hash(newPassword, newSalt)); + roleAccount.setNeedChangePassword(Boolean.FALSE); + roleAccount.setFailedCount(0); + roleAccount.setLockedUntil(null); + roleAccount.setStatus(RoleAccountStatus.ACTIVE.name()); + roleAccountRepository.update(roleAccount); + } + + private void validateRoleStatus(RoleAccountEntity roleAccount) { + if (RoleAccountStatus.UNENABLED.name().equals(roleAccount.getStatus())) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role is not enabled"); + } + if (RoleAccountStatus.LOCKED.name().equals(roleAccount.getStatus()) && isStillLocked(roleAccount)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account is locked"); + } + } + + private boolean isStillLocked(RoleAccountEntity roleAccount) { + return roleAccount.getLockedUntil() == null || roleAccount.getLockedUntil().isAfter(now()); + } + + private void onPasswordFailed(RoleAccountEntity roleAccount) { + int failedCount = roleAccount.getFailedCount() == null ? 0 : roleAccount.getFailedCount(); + failedCount++; + roleAccount.setFailedCount(failedCount); + if (failedCount >= MAX_FAILED_ATTEMPTS) { + roleAccount.setStatus(RoleAccountStatus.LOCKED.name()); + roleAccount.setLockedUntil(now().plusMinutes(IDLE_TIMEOUT_MINUTES)); + roleAccountRepository.update(roleAccount); + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "role account is locked"); + } + roleAccountRepository.update(roleAccount); + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "password is incorrect"); + } + + private void resetFailureState(RoleAccountEntity roleAccount) { + roleAccount.setFailedCount(0); + roleAccount.setLockedUntil(null); + roleAccount.setStatus(RoleAccountStatus.ACTIVE.name()); + roleAccount.setLastLoginAt(now()); + roleAccount.setLastActiveAt(now()); + roleAccountRepository.update(roleAccount); + } + + private AuthLevel resolveAuthLevel(RoleAccountEntity roleAccount, List ukeySerials) { + if (ukeySerials == null || ukeySerials.isEmpty()) { + return AuthLevel.LIMITED; + } + + Set uniqueSerials = new LinkedHashSet<>(ukeySerials); + if (uniqueSerials.size() != roleAccount.getRequiredUkeyCount()) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement"); + } + + List bindings = roleUkeyBindingRepository.findActiveByRoleCode(roleAccount.getRoleCode()); + if (bindings.size() != roleAccount.getRequiredUkeyCount()) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement"); + } + + Set boundSerials = bindings.stream() + .map(RoleUkeyBindingEntity::getUkeySerial) + .collect(java.util.stream.Collectors.toSet()); + if (!boundSerials.containsAll(uniqueSerials)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey verification failed"); + } + return AuthLevel.FULL; + } + + private AuthSessionEntity buildSession(String roleCode, AuthLevel authLevel) { + LocalDateTime issuedAt = now(); + AuthSessionEntity entity = new AuthSessionEntity(); + entity.setRoleCode(roleCode); + entity.setAuthLevel(authLevel.name()); + entity.setSessionToken(tokenSupplier.get()); + entity.setIssuedAt(issuedAt); + entity.setLastActiveAt(issuedAt); + entity.setExpiresAt(issuedAt.plusMinutes(IDLE_TIMEOUT_MINUTES)); + return entity; + } + + private LocalDateTime now() { + return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC); + } + + private AuthSessionEntity requireActiveSession(String sessionToken) { + AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken) + .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session not found")); + if (session.getLogoutAt() != null || session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session expired"); + } + return session; + } } diff --git a/src/main/java/com/cisd/tms/modules/auth/service/CompatAuthService.java b/src/main/java/com/cisd/tms/modules/auth/service/CompatAuthService.java new file mode 100644 index 0000000..54e3341 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/CompatAuthService.java @@ -0,0 +1,158 @@ +package com.cisd.tms.modules.auth.service; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.auth.dto.CompatPasswordLoginRequest; +import com.cisd.tms.modules.auth.dto.CompatUkeyLoginRequest; +import com.cisd.tms.modules.auth.dto.LoginRequest; +import com.cisd.tms.modules.auth.dto.LoginResponse; +import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse; +import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; +import com.cisd.tms.modules.auth.enums.RoleCode; +import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; +import com.cisd.tms.modules.mk.dto.UKeySignDTO; +import com.cisd.tms.modules.mk.dto.UKeySignEntity; +import com.cisd.tms.modules.mk.enums.MasterKeyStatus; +import com.cisd.tms.modules.mk.service.LmkService; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; + +@Service +public class CompatAuthService { + + private final AuthService authService; + private final RoleUkeyBindingRepository roleUkeyBindingRepository; + private final LmkService lmkService; + private final UkeyLoginRandomService ukeyLoginRandomService; + private final CompatUkeyVerifier compatUkeyVerifier; + private final ObjectMapper objectMapper; + + public CompatAuthService( + AuthService authService, + RoleUkeyBindingRepository roleUkeyBindingRepository, + LmkService lmkService, + UkeyLoginRandomService ukeyLoginRandomService, + CompatUkeyVerifier compatUkeyVerifier, + ObjectMapper objectMapper + ) { + this.authService = authService; + this.roleUkeyBindingRepository = roleUkeyBindingRepository; + this.lmkService = lmkService; + this.ukeyLoginRandomService = ukeyLoginRandomService; + this.compatUkeyVerifier = compatUkeyVerifier; + this.objectMapper = objectMapper; + } + + public UkeyLoginRandomResponse issueUkeyLoginRandoms(String legacyRole, Integer count) { + RoleCode roleCode = resolveRoleCode(legacyRole); + int resolvedCount = count == null ? roleCode.getRequiredUkeyCount() : count; + UkeyLoginRandomResponse response = new UkeyLoginRandomResponse(); + response.setRoleCode(roleCode.getCode()); + response.setRandoms(ukeyLoginRandomService.issue(roleCode.getCode(), resolvedCount)); + return response; + } + + public LoginResponse passwordLogin(CompatPasswordLoginRequest request) { + ensureMasterKeyReady(); + RoleCode roleCode = resolveRoleCode(request.getRole()); + LoginRequest loginRequest = new LoginRequest(); + loginRequest.setRoleCode(roleCode.getCode()); + loginRequest.setPassword(request.getPassword()); + return authService.login(loginRequest); + } + + public LoginResponse ukeyLogin(CompatUkeyLoginRequest request) { + ensureMasterKeyReady(); + RoleCode roleCode = resolveRoleCode(request.getRole()); + List activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode()); + validateUkeyCount(roleCode, activeBindings, request.getAuthInfo()); + + Map bindingsByPubKey = activeBindings.stream() + .collect(Collectors.toMap(RoleUkeyBindingEntity::getUkeyPubkey, item -> item, (left, right) -> left, LinkedHashMap::new)); + Set requestPubKeys = request.getAuthInfo().stream() + .map(CompatUkeyLoginRequest.LoginAuthInfo::getPubKey) + .collect(Collectors.toSet()); + if (requestPubKeys.size() != request.getAuthInfo().size() || !bindingsByPubKey.keySet().containsAll(requestPubKeys)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role"); + } + + ukeyLoginRandomService.assertIssued( + roleCode.getCode(), + request.getAuthInfo().stream().map(CompatUkeyLoginRequest.LoginAuthInfo::getRb).toList() + ); + + String authKeyPair = lmkService.exportIkPublicKeyHex(); + List matchedSerials = new ArrayList<>(); + for (CompatUkeyLoginRequest.LoginAuthInfo loginAuthInfo : request.getAuthInfo()) { + RoleUkeyBindingEntity binding = bindingsByPubKey.get(loginAuthInfo.getPubKey()); + compatUkeyVerifier.verifyIssuedBinding(buildIssuePayload(request.getRole(), loginAuthInfo, authKeyPair), loginAuthInfo.getIssueSign()); + compatUkeyVerifier.verifyLoginSignature(loginAuthInfo.getPubKey(), loginAuthInfo.getLoginSignData(), loginAuthInfo.getLoginSign()); + matchedSerials.add(binding.getUkeySerial()); + } + + LoginRequest loginRequest = new LoginRequest(); + loginRequest.setRoleCode(roleCode.getCode()); + loginRequest.setPassword(request.getPassword()); + loginRequest.setUkeySerials(matchedSerials); + return authService.login(loginRequest); + } + + private void ensureMasterKeyReady() { + MasterKeyStatus.StatusDetail status = lmkService.getMasterKeyStatus(); + if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "master key is not ready"); + } + } + + private void validateUkeyCount( + RoleCode roleCode, + List activeBindings, + List authInfos + ) { + if (authInfos == null || authInfos.size() != roleCode.getRequiredUkeyCount()) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement"); + } + if (activeBindings.size() != roleCode.getRequiredUkeyCount()) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement"); + } + } + + private String buildIssuePayload(String legacyRole, CompatUkeyLoginRequest.LoginAuthInfo loginAuthInfo, String authKeyPair) { + UKeySignDTO uKeySignDTO = new UKeySignDTO(); + uKeySignDTO.setPubKey(loginAuthInfo.getPubKey()); + uKeySignDTO.setRole(normalizeLegacyRole(legacyRole)); + uKeySignDTO.setUid(loginAuthInfo.getUid()); + uKeySignDTO.setRid(loginAuthInfo.getRid()); + try { + return objectMapper.writeValueAsString(UKeySignEntity.getInstance(uKeySignDTO, authKeyPair)); + } catch (JsonProcessingException ex) { + throw new IllegalStateException("serialize ukey issue payload failed", ex); + } + } + + private RoleCode resolveRoleCode(String role) { + return switch (normalizeLegacyRole(role)) { + case "superadmin" -> RoleCode.SUPER_ADMIN; + case "keyadmin" -> RoleCode.KEY_ADMIN; + case "auditadmin" -> RoleCode.AUDIT_ADMIN; + case "configadmin", "opsadmin" -> RoleCode.OPS_ADMIN; + case "super_admin" -> RoleCode.SUPER_ADMIN; + case "key_admin" -> RoleCode.KEY_ADMIN; + case "audit_admin" -> RoleCode.AUDIT_ADMIN; + case "ops_admin" -> RoleCode.OPS_ADMIN; + default -> throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "invalid role"); + }; + } + + private String normalizeLegacyRole(String role) { + return role == null ? "" : role.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/CompatUkeyVerifier.java b/src/main/java/com/cisd/tms/modules/auth/service/CompatUkeyVerifier.java new file mode 100644 index 0000000..b9dea67 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/CompatUkeyVerifier.java @@ -0,0 +1,8 @@ +package com.cisd.tms.modules.auth.service; + +public interface CompatUkeyVerifier { + + void verifyIssuedBinding(String payload, String issueSign); + + void verifyLoginSignature(String pubKey, String loginSignData, String loginSign); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/InMemoryUkeyLoginRandomService.java b/src/main/java/com/cisd/tms/modules/auth/service/InMemoryUkeyLoginRandomService.java new file mode 100644 index 0000000..d9201a7 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/InMemoryUkeyLoginRandomService.java @@ -0,0 +1,63 @@ +package com.cisd.tms.modules.auth.service; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import java.time.Clock; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.bouncycastle.util.encoders.Hex; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class InMemoryUkeyLoginRandomService implements UkeyLoginRandomService { + + private static final int RANDOM_BYTES = 16; + private static final int EXPIRE_MINUTES = 10; + + private final PcieCryptoService pcieCryptoService; + private final Clock clock; + private final Map store = new ConcurrentHashMap<>(); + + @Autowired + public InMemoryUkeyLoginRandomService(PcieCryptoService pcieCryptoService) { + this(pcieCryptoService, Clock.systemUTC()); + } + + InMemoryUkeyLoginRandomService(PcieCryptoService pcieCryptoService, Clock clock) { + this.pcieCryptoService = pcieCryptoService; + this.clock = clock; + } + + @Override + public List issue(String roleCode, int count) { + if (count <= 0) { + throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "count must be positive"); + } + List randoms = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + randoms.add(Hex.toHexString(pcieCryptoService.generateRandom(RANDOM_BYTES))); + } + store.put(roleCode, new IssuedRandoms(randoms, clock.instant().plus(EXPIRE_MINUTES, ChronoUnit.MINUTES))); + return randoms; + } + + @Override + public void assertIssued(String roleCode, List randoms) { + IssuedRandoms issuedRandoms = store.remove(roleCode); + if (issuedRandoms == null || issuedRandoms.expiresAt().isBefore(clock.instant())) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login randoms are missing or expired"); + } + if (randoms == null || randoms.size() != issuedRandoms.randoms().size() || !issuedRandoms.randoms().containsAll(randoms)) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login random verification failed"); + } + } + + private record IssuedRandoms(List randoms, Instant expiresAt) { + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/PasswordHasher.java b/src/main/java/com/cisd/tms/modules/auth/service/PasswordHasher.java new file mode 100644 index 0000000..db4cf68 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/PasswordHasher.java @@ -0,0 +1,8 @@ +package com.cisd.tms.modules.auth.service; + +public interface PasswordHasher { + + String hash(String password, String salt); + + boolean matches(String rawPassword, String salt, String expectedHash); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/Pbkdf2PasswordHasher.java b/src/main/java/com/cisd/tms/modules/auth/service/Pbkdf2PasswordHasher.java new file mode 100644 index 0000000..e93da15 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/Pbkdf2PasswordHasher.java @@ -0,0 +1,34 @@ +package com.cisd.tms.modules.auth.service; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.util.Base64; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import org.springframework.stereotype.Component; + +@Component +public class Pbkdf2PasswordHasher implements PasswordHasher { + + private static final int ITERATIONS = 65_536; + private static final int KEY_LENGTH = 256; + + @Override + public String hash(String password, String salt) { + try { + PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8), ITERATIONS, KEY_LENGTH); + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + return Base64.getEncoder().encodeToString(keyFactory.generateSecret(spec).getEncoded()); + } catch (GeneralSecurityException ex) { + throw new IllegalStateException("failed to hash password", ex); + } + } + + @Override + public boolean matches(String rawPassword, String salt, String expectedHash) { + byte[] actual = hash(rawPassword, salt).getBytes(StandardCharsets.UTF_8); + byte[] expected = expectedHash.getBytes(StandardCharsets.UTF_8); + return MessageDigest.isEqual(actual, expected); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/PcieCompatUkeyVerifier.java b/src/main/java/com/cisd/tms/modules/auth/service/PcieCompatUkeyVerifier.java new file mode 100644 index 0000000..0df74c9 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/PcieCompatUkeyVerifier.java @@ -0,0 +1,66 @@ +package com.cisd.tms.modules.auth.service; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds; +import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.mk.service.LmkService; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.bouncycastle.util.encoders.Hex; +import org.springframework.stereotype.Component; + +@Component +public class PcieCompatUkeyVerifier implements CompatUkeyVerifier { + + private final LmkService lmkService; + private final PcieCryptoService pcieCryptoService; + + public PcieCompatUkeyVerifier(LmkService lmkService, PcieCryptoService pcieCryptoService) { + this.lmkService = lmkService; + this.pcieCryptoService = pcieCryptoService; + } + + @Override + public void verifyIssuedBinding(String payload, String issueSign) { + try { + lmkService.verifyIk(payload, issueSign); + } catch (BizException ex) { + throw ex; + } catch (RuntimeException ex) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "issue signature verification failed"); + } + } + + @Override + public void verifyLoginSignature(String pubKey, String loginSignData, String loginSign) { + EccExternalVerifyRequest request = new EccExternalVerifyRequest(); + request.setAlgId(Gm0018AlgorithmIds.SM2_SIGN); + request.setPublicKeyBlob(decodeBlob(pubKey, "login public key is invalid")); + request.setData(loginSignData.getBytes(StandardCharsets.UTF_8)); + request.setSignature(decodeBlob(loginSign, "login signature is invalid")); + try { + pcieCryptoService.eccExternalVerify(request); + } catch (BizException ex) { + throw ex; + } catch (RuntimeException ex) { + throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "login signature verification failed"); + } + } + + private byte[] decodeBlob(String value, String message) { + String normalized = value == null ? "" : value.trim(); + if (normalized.isEmpty()) { + throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), message); + } + if (normalized.matches("(?i)^[0-9a-f]+$") && normalized.length() % 2 == 0) { + return Hex.decode(normalized); + } + try { + return Base64.getDecoder().decode(normalized); + } catch (IllegalArgumentException ex) { + throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), message); + } + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/UkeyLoginRandomService.java b/src/main/java/com/cisd/tms/modules/auth/service/UkeyLoginRandomService.java new file mode 100644 index 0000000..2bfbf2d --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/UkeyLoginRandomService.java @@ -0,0 +1,10 @@ +package com.cisd.tms.modules.auth.service; + +import java.util.List; + +public interface UkeyLoginRandomService { + + List issue(String roleCode, int count); + + void assertIssued(String roleCode, List randoms); +} diff --git a/src/main/java/com/cisd/tms/modules/device/config/DeviceProfileProperties.java b/src/main/java/com/cisd/tms/modules/device/config/DeviceProfileProperties.java new file mode 100644 index 0000000..06fe9da --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/config/DeviceProfileProperties.java @@ -0,0 +1,53 @@ +package com.cisd.tms.modules.device.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "tms.device.profile") +public class DeviceProfileProperties { + + private String model = "SYD7108"; + private String name = "跨境支付终端一体机"; + private String cpuModelDefault = ""; + private String memoryTotalDefault = "16GB"; + private String diskTotalDefault = "256G"; + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCpuModelDefault() { + return cpuModelDefault; + } + + public void setCpuModelDefault(String cpuModelDefault) { + this.cpuModelDefault = cpuModelDefault; + } + + public String getMemoryTotalDefault() { + return memoryTotalDefault; + } + + public void setMemoryTotalDefault(String memoryTotalDefault) { + this.memoryTotalDefault = memoryTotalDefault; + } + + public String getDiskTotalDefault() { + return diskTotalDefault; + } + + public void setDiskTotalDefault(String diskTotalDefault) { + this.diskTotalDefault = diskTotalDefault; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java b/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java index afde97a..7b25299 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/CryptoCardController.java @@ -1,15 +1,22 @@ package com.cisd.tms.modules.device.controller; +import static com.cisd.tms.modules.device.support.crypto.CryptoPayloadCodec.*; +import static com.cisd.tms.modules.device.support.crypto.CryptoRequestMapper.*; + import com.cisd.tms.common.api.ApiResponse; +import com.cisd.tms.modules.device.dto.crypto.request.*; import com.cisd.tms.integration.crypto.pcie.model.AgreementDataAndKeyEccResult; +import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; import com.cisd.tms.integration.crypto.pcie.model.DeviceConfResult; import com.cisd.tms.integration.crypto.pcie.model.DeviceCountResult; import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult; +import com.cisd.tms.integration.crypto.pcie.model.DeviceStatusResult; import com.cisd.tms.integration.crypto.pcie.model.DigestRequest; import com.cisd.tms.integration.crypto.pcie.model.EccAgreementSessionKeyRequest; import com.cisd.tms.integration.crypto.pcie.model.EccAgreementSessionKeyResult; import com.cisd.tms.integration.crypto.pcie.model.ExchangeDigitEnvelopeEccRequest; import com.cisd.tms.integration.crypto.pcie.model.ExchangeDigitEnvelopeRsaRequest; +import com.cisd.tms.integration.crypto.pcie.model.ExportKekComponentRequest; import com.cisd.tms.integration.crypto.pcie.model.FileCreateRequest; import com.cisd.tms.integration.crypto.pcie.model.FileReadRequest; import com.cisd.tms.integration.crypto.pcie.model.FileWriteRequest; @@ -20,18 +27,18 @@ import com.cisd.tms.integration.crypto.pcie.model.GenerateStandardRsaKeyPairRequ import com.cisd.tms.integration.crypto.pcie.model.HmacRequest; import com.cisd.tms.integration.crypto.pcie.model.HmacResult; import com.cisd.tms.integration.crypto.pcie.model.KeyStatusResult; +import com.cisd.tms.integration.crypto.pcie.model.LoginStatusResult; import com.cisd.tms.integration.crypto.pcie.model.MacCalcRequest; import com.cisd.tms.integration.crypto.pcie.model.SymmetricCryptoRequest; import com.cisd.tms.integration.crypto.pcie.model.SymmetricKekCryptoRequest; import com.cisd.tms.integration.crypto.pcie.model.StandardKeyPairResult; +import com.cisd.tms.integration.crypto.pcie.service.PcieDeviceStatusFormatter; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotBlank; -import java.util.Arrays; import java.util.Base64; import java.util.Map; import org.springframework.validation.annotation.Validated; @@ -381,803 +388,383 @@ public class CryptoCardController { )); } - private static SymmetricCryptoRequest toSymmetricCryptoRequest(CryptoSymmetricInternalRequest request) { - SymmetricCryptoRequest cryptoRequest = new SymmetricCryptoRequest(); - cryptoRequest.setAlgId(request.getAlgId()); - cryptoRequest.setKey(Base64.getDecoder().decode(request.getKeyBase64())); - cryptoRequest.setIv(decodeNullableBase64(request.getIvBase64())); - cryptoRequest.setData(Base64.getDecoder().decode(request.getDataBase64())); - cryptoRequest.setOutBufferSize(request.getOutBufferSize()); - return cryptoRequest; + @PostMapping("/device/init-identify") + @Operation(summary = "初始化识别码", description = "使用旧 PIN 和新 PIN 初始化设备识别码。") + public ApiResponse initIdentify(@Valid @RequestBody CryptoInitIdentifyInternalRequest request) { + pcieCryptoService.initIdentify( + decodeBase64(request.getOldPinBase64()), + decodeBase64(request.getNewPinBase64()) + ); + return ApiResponse.success(); } - private static SymmetricKekCryptoRequest toSymmetricKekCryptoRequest(CryptoSymmetricKekInternalRequest request) { - SymmetricKekCryptoRequest cryptoRequest = new SymmetricKekCryptoRequest(); - cryptoRequest.setAlgId(request.getAlgId()); - cryptoRequest.setKekIndex(request.getKekIndex()); - cryptoRequest.setWrappedKey(Base64.getDecoder().decode(request.getWrappedKeyBase64())); - cryptoRequest.setIv(decodeNullableBase64(request.getIvBase64())); - cryptoRequest.setData(Base64.getDecoder().decode(request.getDataBase64())); - cryptoRequest.setOutBufferSize(request.getOutBufferSize()); - return cryptoRequest; + @GetMapping("/device/status") + @Operation(summary = "查询设备状态", description = "返回密码卡扩展设备状态原始结构体数据,按 HEX 编码。") + public ApiResponse getDeviceStatus() { + DeviceStatusResult result = pcieCryptoService.getDeviceStatus(); + PcieDeviceStatusFormatter.ensureHex(result); + return ApiResponse.success(result); } - private static byte[] decodeNullableBase64(String value) { - if (value == null || value.isBlank()) { - return null; - } - byte[] decoded = Base64.getDecoder().decode(value); - return decoded.length == 0 ? null : Arrays.copyOf(decoded, decoded.length); + @PostMapping("/device/factory-reset") + @Operation(summary = "执行设备恢复出厂", description = "触发密码卡扩展恢复出厂操作。") + public ApiResponse deviceFactoryReset() { + pcieCryptoService.deviceFactoryReset(); + return ApiResponse.success(); } - public static class CryptoHmacInternalRequest { - - @Min(1) - private int algId; - - @NotBlank - private String keyBase64; - - @NotBlank - private String dataBase64; - - public int getAlgId() { - return algId; - } - - public void setAlgId(int algId) { - this.algId = algId; - } - - public String getKeyBase64() { - return keyBase64; - } - - public void setKeyBase64(String keyBase64) { - this.keyBase64 = keyBase64; - } - - public String getDataBase64() { - return dataBase64; - } - - public void setDataBase64(String dataBase64) { - this.dataBase64 = dataBase64; - } + @PostMapping("/private-key/password/change") + @Operation(summary = "修改私钥访问口令", description = "修改指定索引私钥的访问口令。") + public ApiResponse changePrivateKeyPassword( + @Valid @RequestBody CryptoChangePrivateKeyPasswordInternalRequest request + ) { + pcieCryptoService.changePrivateKeyAccessPassword(toChangePrivateKeyPasswordRequest(request)); + return ApiResponse.success(); } - public static class CryptoGenerateKekInternalRequest { - - @Min(1) - private int keyBits; - - @Min(0) - private int keyIndex; - - public int getKeyBits() { - return keyBits; - } - - public void setKeyBits(int keyBits) { - this.keyBits = keyBits; - } - - public int getKeyIndex() { - return keyIndex; - } - - public void setKeyIndex(int keyIndex) { - this.keyIndex = keyIndex; - } + @PostMapping("/private-key/access/open") + @Operation(summary = "获取私钥访问权限", description = "使用口令打开指定索引私钥的访问权限。") + public ApiResponse getPrivateKeyAccessRight( + @Valid @RequestBody CryptoPrivateKeyAccessInternalRequest request + ) { + pcieCryptoService.getPrivateKeyAccessRight(toPrivateKeyAccessRequest(request)); + return ApiResponse.success(); } - public static class CryptoGenerateStandardRsaKeyPairInternalRequest { - - @Min(1) - private int keyBits; - - @Min(1) - private int publicKeyBufferSize; - - @Min(1) - private int privateKeyBufferSize; - - public int getKeyBits() { - return keyBits; - } - - public void setKeyBits(int keyBits) { - this.keyBits = keyBits; - } - - public int getPublicKeyBufferSize() { - return publicKeyBufferSize; - } - - public void setPublicKeyBufferSize(int publicKeyBufferSize) { - this.publicKeyBufferSize = publicKeyBufferSize; - } - - public int getPrivateKeyBufferSize() { - return privateKeyBufferSize; - } - - public void setPrivateKeyBufferSize(int privateKeyBufferSize) { - this.privateKeyBufferSize = privateKeyBufferSize; - } + @PostMapping("/private-key/access/release") + @Operation(summary = "释放私钥访问权限", description = "释放指定索引私钥的访问权限。") + public ApiResponse releasePrivateKeyAccessRight( + @RequestParam @Min(0) int keyIndex + ) { + pcieCryptoService.releasePrivateKeyAccessRight(keyIndex); + return ApiResponse.success(); } - public static class CryptoGenerateStandardEccKeyPairInternalRequest { - - @Min(1) - private int algId; - - @Min(1) - private int keyBits; - - @Min(1) - private int publicKeyBufferSize; - - @Min(1) - private int privateKeyBufferSize; - - public int getAlgId() { - return algId; - } - - public void setAlgId(int algId) { - this.algId = algId; - } - - public int getKeyBits() { - return keyBits; - } - - public void setKeyBits(int keyBits) { - this.keyBits = keyBits; - } - - public int getPublicKeyBufferSize() { - return publicKeyBufferSize; - } - - public void setPublicKeyBufferSize(int publicKeyBufferSize) { - this.publicKeyBufferSize = publicKeyBufferSize; - } - - public int getPrivateKeyBufferSize() { - return privateKeyBufferSize; - } - - public void setPrivateKeyBufferSize(int privateKeyBufferSize) { - this.privateKeyBufferSize = privateKeyBufferSize; - } + @PostMapping("/keypair/ecc/generate") + @Operation(summary = "生成扩展 ECC 密钥对", description = "在指定槽位生成扩展 ECC 密钥对。") + public ApiResponse generateKeyPairEcc( + @Valid @RequestBody CryptoGenerateKeyPairInternalRequest request + ) { + pcieCryptoService.generateKeyPairEcc(toGenerateKeyPairRequest(request)); + return ApiResponse.success(); } - public static class CryptoExchangeEnvelopeRsaInternalRequest { - - @Min(0) - private int keyIndex; - - @NotBlank - private String publicKeyBase64; - - @NotBlank - private String inputEnvelopeBase64; - - @Min(1) - private int outBufferSize; - - public int getKeyIndex() { - return keyIndex; - } - - public void setKeyIndex(int keyIndex) { - this.keyIndex = keyIndex; - } - - public String getPublicKeyBase64() { - return publicKeyBase64; - } - - public void setPublicKeyBase64(String publicKeyBase64) { - this.publicKeyBase64 = publicKeyBase64; - } - - public String getInputEnvelopeBase64() { - return inputEnvelopeBase64; - } - - public void setInputEnvelopeBase64(String inputEnvelopeBase64) { - this.inputEnvelopeBase64 = inputEnvelopeBase64; - } - - public int getOutBufferSize() { - return outBufferSize; - } - - public void setOutBufferSize(int outBufferSize) { - this.outBufferSize = outBufferSize; - } + @PostMapping("/keypair/rsa/generate") + @Operation(summary = "生成扩展 RSA 密钥对", description = "在指定槽位生成扩展 RSA 密钥对。") + public ApiResponse generateKeyPairRsa( + @Valid @RequestBody CryptoGenerateKeyPairInternalRequest request + ) { + pcieCryptoService.generateKeyPairRsa(toGenerateKeyPairRequest(request)); + return ApiResponse.success(); } - public static class CryptoExchangeEnvelopeEccInternalRequest { - - @Min(0) - private int keyIndex; - - @Min(1) - private int algId; - - @NotBlank - private String publicKeyBase64; - - @NotBlank - private String inputEnvelopeBase64; - - @Min(1) - private int outBufferSize; - - public int getKeyIndex() { - return keyIndex; - } - - public void setKeyIndex(int keyIndex) { - this.keyIndex = keyIndex; - } - - public int getAlgId() { - return algId; - } - - public void setAlgId(int algId) { - this.algId = algId; - } - - public String getPublicKeyBase64() { - return publicKeyBase64; - } - - public void setPublicKeyBase64(String publicKeyBase64) { - this.publicKeyBase64 = publicKeyBase64; - } - - public String getInputEnvelopeBase64() { - return inputEnvelopeBase64; - } - - public void setInputEnvelopeBase64(String inputEnvelopeBase64) { - this.inputEnvelopeBase64 = inputEnvelopeBase64; - } - - public int getOutBufferSize() { - return outBufferSize; - } - - public void setOutBufferSize(int outBufferSize) { - this.outBufferSize = outBufferSize; - } + @PostMapping("/keypair/ecc/import") + @Operation(summary = "导入扩展 ECC 密钥对", description = "向指定槽位导入扩展 ECC 公私钥。") + public ApiResponse importKeyPairEcc( + @Valid @RequestBody CryptoImportKeyPairInternalRequest request + ) { + pcieCryptoService.importKeyPairEcc(toImportKeyPairRequest(request)); + return ApiResponse.success(); } - public static class CryptoGenerateAgreementDataAndKeyEccInternalRequest { - - @Min(0) - private int iskIndex; - - @Min(1) - private int keyBits; - - @NotBlank - private String responseIdBase64; - - @NotBlank - private String sponsorIdBase64; - - @NotBlank - private String sponsorPublicKeyBase64; - - @NotBlank - private String sponsorTmpPublicKeyBase64; - - @Min(1) - private int responsePublicKeyBufferSize; - - @Min(1) - private int responseTmpPublicKeyBufferSize; - - public int getIskIndex() { - return iskIndex; - } - - public void setIskIndex(int iskIndex) { - this.iskIndex = iskIndex; - } - - public int getKeyBits() { - return keyBits; - } - - public void setKeyBits(int keyBits) { - this.keyBits = keyBits; - } - - public String getResponseIdBase64() { - return responseIdBase64; - } - - public void setResponseIdBase64(String responseIdBase64) { - this.responseIdBase64 = responseIdBase64; - } - - public String getSponsorIdBase64() { - return sponsorIdBase64; - } - - public void setSponsorIdBase64(String sponsorIdBase64) { - this.sponsorIdBase64 = sponsorIdBase64; - } - - public String getSponsorPublicKeyBase64() { - return sponsorPublicKeyBase64; - } - - public void setSponsorPublicKeyBase64(String sponsorPublicKeyBase64) { - this.sponsorPublicKeyBase64 = sponsorPublicKeyBase64; - } - - public String getSponsorTmpPublicKeyBase64() { - return sponsorTmpPublicKeyBase64; - } - - public void setSponsorTmpPublicKeyBase64(String sponsorTmpPublicKeyBase64) { - this.sponsorTmpPublicKeyBase64 = sponsorTmpPublicKeyBase64; - } - - public int getResponsePublicKeyBufferSize() { - return responsePublicKeyBufferSize; - } - - public void setResponsePublicKeyBufferSize(int responsePublicKeyBufferSize) { - this.responsePublicKeyBufferSize = responsePublicKeyBufferSize; - } - - public int getResponseTmpPublicKeyBufferSize() { - return responseTmpPublicKeyBufferSize; - } - - public void setResponseTmpPublicKeyBufferSize(int responseTmpPublicKeyBufferSize) { - this.responseTmpPublicKeyBufferSize = responseTmpPublicKeyBufferSize; - } + @PostMapping("/keypair/rsa/import") + @Operation(summary = "导入扩展 RSA 密钥对", description = "向指定槽位导入扩展 RSA 公私钥。") + public ApiResponse importKeyPairRsa( + @Valid @RequestBody CryptoImportKeyPairInternalRequest request + ) { + pcieCryptoService.importKeyPairRsa(toImportKeyPairRequest(request)); + return ApiResponse.success(); } - public static class CryptoGenerateSessionKeyEccInternalRequest { - - @Min(0) - private int iskIndex; - - @Min(1) - private int keyBits; - - @NotBlank - private String sponsorIdBase64; - - @NotBlank - private String responseIdBase64; - - @NotBlank - private String responsePublicKeyBase64; - - @NotBlank - private String responseTmpPublicKeyBase64; - - @Min(1) - private int sponsorPublicKeyBufferSize; - - @Min(1) - private int sponsorTmpPublicKeyBufferSize; - - @Min(1) - private int sessionKeyBufferSize; - - public int getIskIndex() { - return iskIndex; - } - - public void setIskIndex(int iskIndex) { - this.iskIndex = iskIndex; - } - - public int getKeyBits() { - return keyBits; - } - - public void setKeyBits(int keyBits) { - this.keyBits = keyBits; - } - - public String getSponsorIdBase64() { - return sponsorIdBase64; - } - - public void setSponsorIdBase64(String sponsorIdBase64) { - this.sponsorIdBase64 = sponsorIdBase64; - } - - public String getResponseIdBase64() { - return responseIdBase64; - } - - public void setResponseIdBase64(String responseIdBase64) { - this.responseIdBase64 = responseIdBase64; - } - - public String getResponsePublicKeyBase64() { - return responsePublicKeyBase64; - } - - public void setResponsePublicKeyBase64(String responsePublicKeyBase64) { - this.responsePublicKeyBase64 = responsePublicKeyBase64; - } - - public String getResponseTmpPublicKeyBase64() { - return responseTmpPublicKeyBase64; - } - - public void setResponseTmpPublicKeyBase64(String responseTmpPublicKeyBase64) { - this.responseTmpPublicKeyBase64 = responseTmpPublicKeyBase64; - } - - public int getSponsorPublicKeyBufferSize() { - return sponsorPublicKeyBufferSize; - } - - public void setSponsorPublicKeyBufferSize(int sponsorPublicKeyBufferSize) { - this.sponsorPublicKeyBufferSize = sponsorPublicKeyBufferSize; - } - - public int getSponsorTmpPublicKeyBufferSize() { - return sponsorTmpPublicKeyBufferSize; - } - - public void setSponsorTmpPublicKeyBufferSize(int sponsorTmpPublicKeyBufferSize) { - this.sponsorTmpPublicKeyBufferSize = sponsorTmpPublicKeyBufferSize; - } - - public int getSessionKeyBufferSize() { - return sessionKeyBufferSize; - } - - public void setSessionKeyBufferSize(int sessionKeyBufferSize) { - this.sessionKeyBufferSize = sessionKeyBufferSize; - } + @PostMapping("/kek/import") + @Operation(summary = "导入 KEK", description = "向指定索引导入 KEK 明文密钥数据。") + public ApiResponse importKek(@Valid @RequestBody CryptoImportKekInternalRequest request) { + pcieCryptoService.importKek(toImportKekRequest(request)); + return ApiResponse.success(); } - public static class CryptoDigestInternalRequest { - - @Min(1) - private int algId; - - @NotBlank - private String dataBase64; - - @Min(1) - private int outBufferSize = 64; - - public int getAlgId() { - return algId; - } - - public void setAlgId(int algId) { - this.algId = algId; - } - - public String getDataBase64() { - return dataBase64; - } - - public void setDataBase64(String dataBase64) { - this.dataBase64 = dataBase64; - } - - public int getOutBufferSize() { - return outBufferSize; - } - - public void setOutBufferSize(int outBufferSize) { - this.outBufferSize = outBufferSize; - } + @PostMapping("/kek/component/import") + @Operation(summary = "导入 KEK 分量", description = "向指定 KEK 索引和分量号导入分量数据。") + public ApiResponse importKekComponent( + @Valid @RequestBody CryptoKekComponentImportInternalRequest request + ) { + pcieCryptoService.importKekComponent(toKekComponentRequest(request)); + return ApiResponse.success(); } - public static class CryptoSymmetricInternalRequest { - - @Min(1) - private int algId; - - @NotBlank - private String keyBase64; - - private String ivBase64; - - @NotBlank - private String dataBase64; - - @Min(1) - private int outBufferSize; - - public int getAlgId() { - return algId; - } - - public void setAlgId(int algId) { - this.algId = algId; - } - - public String getKeyBase64() { - return keyBase64; - } - - public void setKeyBase64(String keyBase64) { - this.keyBase64 = keyBase64; - } - - public String getIvBase64() { - return ivBase64; - } - - public void setIvBase64(String ivBase64) { - this.ivBase64 = ivBase64; - } - - public String getDataBase64() { - return dataBase64; - } - - public void setDataBase64(String dataBase64) { - this.dataBase64 = dataBase64; - } - - public int getOutBufferSize() { - return outBufferSize; - } - - public void setOutBufferSize(int outBufferSize) { - this.outBufferSize = outBufferSize; - } + @PostMapping("/kek/component/export") + @Operation(summary = "导出 KEK 分量", description = "导出指定 KEK 分量,返回 HEX 数据。") + public ApiResponse> exportKekComponent( + @Valid @RequestBody CryptoKekComponentExportInternalRequest request + ) { + ExportKekComponentRequest req = new ExportKekComponentRequest(); + req.setKeyIndex(request.getKeyIndex()); + req.setComponentNum(request.getComponentNum()); + req.setComponentSize(request.getComponentSize()); + BackupDataResult result = pcieCryptoService.exportKekComponent(req); + return ApiResponse.success(hexPayload("componentHex", result.getData(), result.getLength())); } - public static class CryptoSymmetricKekInternalRequest { - - @Min(1) - private int algId; - - @Min(0) - private int kekIndex; - - @NotBlank - private String wrappedKeyBase64; - - private String ivBase64; - - @NotBlank - private String dataBase64; - - @Min(1) - private int outBufferSize; - - public int getAlgId() { - return algId; - } - - public void setAlgId(int algId) { - this.algId = algId; - } - - public int getKekIndex() { - return kekIndex; - } - - public void setKekIndex(int kekIndex) { - this.kekIndex = kekIndex; - } - - public String getWrappedKeyBase64() { - return wrappedKeyBase64; - } - - public void setWrappedKeyBase64(String wrappedKeyBase64) { - this.wrappedKeyBase64 = wrappedKeyBase64; - } - - public String getIvBase64() { - return ivBase64; - } - - public void setIvBase64(String ivBase64) { - this.ivBase64 = ivBase64; - } - - public String getDataBase64() { - return dataBase64; - } - - public void setDataBase64(String dataBase64) { - this.dataBase64 = dataBase64; - } - - public int getOutBufferSize() { - return outBufferSize; - } - - public void setOutBufferSize(int outBufferSize) { - this.outBufferSize = outBufferSize; - } + @PostMapping("/user-key/backup") + @Operation(summary = "备份用户密钥", description = "备份指定用户密钥,返回 HEX 数据。") + public ApiResponse> backupUserKey( + @Valid @RequestBody CryptoBackupUserKeyInternalRequest request + ) { + BackupDataResult result = pcieCryptoService.backupUserKey( + request.getKeyIndex(), + request.getKeyType(), + request.getOutBufferSize() + ); + return ApiResponse.success(hexPayload("dataHex", result.getData(), result.getLength())); } - public static class CryptoMacInternalRequest { - - @Min(1) - private int algId; - - @NotBlank - private String keyBase64; - - private String ivBase64; - - @NotBlank - private String dataBase64; - - @Min(1) - private int outBufferSize = 32; - - public int getAlgId() { - return algId; - } - - public void setAlgId(int algId) { - this.algId = algId; - } - - public String getKeyBase64() { - return keyBase64; - } - - public void setKeyBase64(String keyBase64) { - this.keyBase64 = keyBase64; - } - - public String getIvBase64() { - return ivBase64; - } - - public void setIvBase64(String ivBase64) { - this.ivBase64 = ivBase64; - } - - public String getDataBase64() { - return dataBase64; - } - - public void setDataBase64(String dataBase64) { - this.dataBase64 = dataBase64; - } - - public int getOutBufferSize() { - return outBufferSize; - } - - public void setOutBufferSize(int outBufferSize) { - this.outBufferSize = outBufferSize; - } + @PostMapping("/user-key/recover") + @Operation(summary = "恢复用户密钥", description = "从备份数据恢复用户密钥。") + public ApiResponse recoverUserKey( + @Valid @RequestBody CryptoRecoverUserKeyInternalRequest request + ) { + pcieCryptoService.recoverUserKey(toRecoverUserKeyRequest(request)); + return ApiResponse.success(); } - public static class CryptoFileCreateInternalRequest { - - @NotBlank - private String fileName; - - @Min(1) - private int fileSize; - - public String getFileName() { - return fileName; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - } - - public int getFileSize() { - return fileSize; - } - - public void setFileSize(int fileSize) { - this.fileSize = fileSize; - } + @PostMapping("/kek/backup") + @Operation(summary = "备份 KEK", description = "备份指定索引 KEK,返回 HEX 数据。") + public ApiResponse> backupKek( + @RequestParam @Min(0) int keyIndex, + @RequestParam @Min(1) int outBufferSize + ) { + BackupDataResult result = pcieCryptoService.backupKek(keyIndex, outBufferSize); + return ApiResponse.success(hexPayload("dataHex", result.getData(), result.getLength())); } - public static class CryptoFileWriteInternalRequest { - - @NotBlank - private String fileName; - - @Min(0) - private int offset; - - @NotBlank - private String dataBase64; - - public String getFileName() { - return fileName; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - } - - public int getOffset() { - return offset; - } - - public void setOffset(int offset) { - this.offset = offset; - } - - public String getDataBase64() { - return dataBase64; - } - - public void setDataBase64(String dataBase64) { - this.dataBase64 = dataBase64; - } + @PostMapping("/kek/recover") + @Operation(summary = "恢复 KEK", description = "从备份数据恢复 KEK。") + public ApiResponse recoverKek(@Valid @RequestBody CryptoRecoverKekInternalRequest request) { + pcieCryptoService.recoverKek(toRecoverKekRequest(request)); + return ApiResponse.success(); } - public static class CryptoFileReadInternalRequest { - - @NotBlank - private String fileName; - - @Min(0) - private int offset; - - @Min(1) - private int readLength; - - public String getFileName() { - return fileName; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - } - - public int getOffset() { - return offset; - } - - public void setOffset(int offset) { - this.offset = offset; - } - - public int getReadLength() { - return readLength; - } - - public void setReadLength(int readLength) { - this.readLength = readLength; - } + @PostMapping("/user-key/delete") + @Operation(summary = "删除用户密钥", description = "删除指定索引用户密钥。") + public ApiResponse deleteUserKey(@RequestParam @Min(0) int keyIndex) { + pcieCryptoService.deleteUserKey(keyIndex); + return ApiResponse.success(); } - public static class CryptoFileDeleteInternalRequest { + @PostMapping("/kek/delete") + @Operation(summary = "删除 KEK", description = "删除指定索引 KEK。") + public ApiResponse deleteKek(@RequestParam @Min(0) int keyIndex) { + pcieCryptoService.deleteKek(keyIndex); + return ApiResponse.success(); + } - @NotBlank - private String fileName; + @PostMapping("/keys/backup-all") + @Operation(summary = "备份全部密钥", description = "备份整卡全部密钥数据,返回 HEX 数据。") + public ApiResponse> backupAllKeys(@RequestParam @Min(1) int outBufferSize) { + BackupDataResult result = pcieCryptoService.backupAllKey(outBufferSize); + return ApiResponse.success(hexPayload("dataHex", result.getData(), result.getLength())); + } - public String getFileName() { - return fileName; - } + @PostMapping("/keys/restore-all") + @Operation(summary = "恢复全部密钥", description = "使用整卡备份数据恢复全部密钥。") + public ApiResponse restoreAllKeys(@Valid @RequestBody CryptoRestoreAllKeysInternalRequest request) { + pcieCryptoService.restoreAllKey(decodeBase64(request.getKeyImageBase64())); + return ApiResponse.success(); + } - public void setFileName(String fileName) { - this.fileName = fileName; - } + @GetMapping("/user-key/status/ecc") + @Operation(summary = "查询 ECC 用户密钥状态", description = "返回指定 ECC 用户密钥槽位状态。") + public ApiResponse statusUserKeyEcc( + @RequestParam @Min(0) int keyIndex, + @RequestParam @Min(0) int keyType + ) { + return ApiResponse.success(pcieCryptoService.statusUserKeyEcc(keyIndex, keyType)); + } + + @GetMapping("/user-key/status/rsa") + @Operation(summary = "查询 RSA 用户密钥状态", description = "返回指定 RSA 用户密钥槽位状态。") + public ApiResponse statusUserKeyRsa( + @RequestParam @Min(0) int keyIndex, + @RequestParam @Min(0) int keyType + ) { + return ApiResponse.success(pcieCryptoService.statusUserKeyRsa(keyIndex, keyType)); + } + + @PostMapping("/keypair/ecc/import-encrypted") + @Operation(summary = "导入加密 ECC 密钥对", description = "导入加密封装的 ECC 密钥对数据。") + public ApiResponse importEncryptedKeyPairEcc( + @Valid @RequestBody CryptoImportEncryptedKeyPairEccInternalRequest request + ) { + pcieCryptoService.importEncryptedKeyPairEcc(toImportEncryptedKeyPairEccRequest(request)); + return ApiResponse.success(); + } + + @PostMapping("/keypair/rsa/import-encrypted") + @Operation(summary = "导入加密 RSA 密钥对", description = "导入包裹密钥保护的 RSA 密钥对数据。") + public ApiResponse importEncryptedKeyPairRsa( + @Valid @RequestBody CryptoImportEncryptedKeyPairRsaInternalRequest request + ) { + pcieCryptoService.importEncryptedKeyPairRsa(toImportEncryptedKeyPairRsaRequest(request)); + return ApiResponse.success(); + } + + @PostMapping("/keys/update-backed") + @Operation(summary = "更新备份数据", description = "更新旧备份结构并返回新的备份数据 HEX。") + public ApiResponse> updateBackedKey( + @Valid @RequestBody CryptoUpdateBackedKeyInternalRequest request + ) { + BackupDataResult result = pcieCryptoService.updateBackedKey(toUpdateBackedKeyRequest(request)); + return ApiResponse.success(hexPayload("dataHex", result.getData(), result.getLength())); + } + + @PostMapping("/lmk/generate") + @Operation(summary = "生成 LMK", description = "生成设备 LMK。") + public ApiResponse generateLmk() { + pcieCryptoService.generateLmk(); + return ApiResponse.success(); + } + + @PostMapping("/lmk/load") + @Operation(summary = "加载 LMK", description = "加载设备 LMK。") + public ApiResponse loadLmk() { + pcieCryptoService.loadLmk(); + return ApiResponse.success(); + } + + @PostMapping("/lmk/backup") + @Operation(summary = "备份 LMK", description = "触发设备 LMK 备份。") + public ApiResponse backupLmk() { + pcieCryptoService.backupLmk(); + return ApiResponse.success(); + } + + @PostMapping("/lmk/recover") + @Operation(summary = "恢复 LMK", description = "触发设备 LMK 恢复。") + public ApiResponse recoverLmk() { + pcieCryptoService.recoverLmk(); + return ApiResponse.success(); + } + + @PostMapping("/lmk/destroy") + @Operation(summary = "销毁 LMK", description = "销毁设备 LMK。") + public ApiResponse destroyLmk() { + pcieCryptoService.destroyLmk(); + return ApiResponse.success(); + } + + @PostMapping("/lmk/backup-ex") + @Operation(summary = "导出 LMK 扩展备份", description = "导出扩展 LMK 备份分量,返回 HEX 数据。") + public ApiResponse> backupLmkEx() { + BackupDataResult result = pcieCryptoService.backupLmkEx(); + return ApiResponse.success(lmkComponentPayload(result)); + } + + @PostMapping("/lmk/recover-ex") + @Operation(summary = "恢复 LMK 扩展备份", description = "使用扩展 LMK 分量恢复 LMK。") + public ApiResponse recoverLmkEx(@Valid @RequestBody CryptoRecoverLmkExInternalRequest request) { + //todo 恢复管理密钥后需要进行加载 + pcieCryptoService.recoverLmkEx(toLmkComponentBytes(request)); + return ApiResponse.success(); + } + + @PostMapping("/ik/generate") + @Operation(summary = "生成 IK", description = "生成指定类型的 IK。") + public ApiResponse generateIk(@RequestParam @Min(0) int keyType) { + pcieCryptoService.generateIk(keyType); + return ApiResponse.success(); + } + + @PostMapping("/ik/backup") + @Operation(summary = "备份 IK", description = "备份指定类型的 IK。") + public ApiResponse backupIk(@RequestParam @Min(0) int keyType) { + pcieCryptoService.backupIk(keyType); + return ApiResponse.success(); + } + + @PostMapping("/ik/recover") + @Operation(summary = "恢复 IK", description = "恢复指定类型的 IK。") + public ApiResponse recoverIk(@RequestParam @Min(0) int keyType) { + pcieCryptoService.recoverIk(keyType); + return ApiResponse.success(); + } + + @PostMapping("/ik/destroy") + @Operation(summary = "销毁 IK", description = "销毁指定类型的 IK。") + public ApiResponse destroyIk(@RequestParam @Min(0) int keyType) { + pcieCryptoService.destroyIk(keyType); + return ApiResponse.success(); + } + + @GetMapping("/ik/status") + @Operation(summary = "查询 IK 状态", description = "返回指定类型 IK 的状态。") + public ApiResponse statusIk(@RequestParam @Min(0) int keyType) { + return ApiResponse.success(pcieCryptoService.statusIk(keyType)); + } + + @PostMapping("/ukey/generate") + @Operation(summary = "生成 UKey", description = "生成 UKey 并初始化管理员与登录 PIN。") + public ApiResponse generateUkey(@Valid @RequestBody CryptoUkeyGenerateInternalRequest request) { + pcieCryptoService.generateUkey(toUkeyGenerateRequest(request)); + return ApiResponse.success(); + } + + @PostMapping("/ukey/login") + @Operation(summary = "UKey 登录", description = "使用 UKey PIN 和登录 PIN 完成 UKey 登录。") + public ApiResponse loginUkey(@Valid @RequestBody CryptoUkeyLoginInternalRequest request) { + pcieCryptoService.loginUkey(toUkeyLoginRequest(request)); + return ApiResponse.success(); + } + + @PostMapping("/ukey/pin/change") + @Operation(summary = "修改 UKey PIN", description = "修改 UKey 用户或管理员 PIN。") + public ApiResponse changeUkeyPin(@Valid @RequestBody CryptoUkeyPinChangeInternalRequest request) { + pcieCryptoService.changeUkeyPin(toUkeyPinChangeRequest(request)); + return ApiResponse.success(); + } + + @PostMapping("/ukey/unlock") + @Operation(summary = "解锁 UKey", description = "使用管理员 PIN 和用户 PIN 解锁 UKey。") + public ApiResponse unlockUkey(@Valid @RequestBody CryptoUkeyUnlockInternalRequest request) { + pcieCryptoService.unlockUkey(toUkeyUnlockRequest(request)); + return ApiResponse.success(); + } + + @PostMapping("/ukey/lmk-component/read") + @Operation(summary = "读取 UKey LMK 分量", description = "读取并处理 UKey LMK 分量。") + public ApiResponse readLmkComponent(@Valid @RequestBody CryptoUkeyPinInternalRequest request) { + pcieCryptoService.readLmkComponent(decodeBase64(request.getUkeyPinBase64())); + return ApiResponse.success(); + } + + @PostMapping("/ukey/ik-component/read") + @Operation(summary = "读取 UKey IK 分量", description = "按 keyType 读取并处理 UKey IK 分量。") + public ApiResponse readUkeyIkComponent( + @Valid @RequestBody CryptoReadUkeyIkComponentInternalRequest request + ) { + pcieCryptoService.readUkeyIkComponent( + request.getKeyType(), + decodeBase64(request.getUkeyPinBase64()) + ); + return ApiResponse.success(); + } + + @PostMapping("/ukey/certificate/generate") + @Operation(summary = "生成 UKey 证书", description = "使用 UKey PIN 生成 UKey 证书。") + public ApiResponse generateCertificate(@Valid @RequestBody CryptoUkeyPinInternalRequest request) { + pcieCryptoService.generateCertificate(decodeBase64(request.getUkeyPinBase64())); + return ApiResponse.success(); + } + + @PostMapping("/ukey/logout-admin") + @Operation(summary = "管理员退出 UKey", description = "指定管理员索引执行 UKey 退出。") + public ApiResponse logoutAdmin(@RequestParam @Min(0) int adminIndex) { + pcieCryptoService.logoutAdmin(adminIndex); + return ApiResponse.success(); + } + + @GetMapping("/ukey/login-status") + @Operation(summary = "查询 UKey 登录状态", description = "返回三个管理员槽位的 UKey 登录状态。") + public ApiResponse loginStatus() { + return ApiResponse.success(pcieCryptoService.loginStatus()); } } diff --git a/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java b/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java index b1590e5..45b8250 100644 --- a/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java +++ b/src/main/java/com/cisd/tms/modules/device/controller/DeviceController.java @@ -8,12 +8,13 @@ import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse; import com.cisd.tms.modules.device.service.DeviceProfileService; import com.cisd.tms.modules.device.service.DeviceRuntimeStatusService; import com.cisd.tms.modules.device.service.DeviceService; +import com.cisd.tms.security.internal.InternalApiAuthInterceptor; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -62,10 +63,8 @@ public class DeviceController { @PostMapping("/restart") @Operation(summary = "重启设备服务", description = "受理设备重启请求,并记录触发操作的内部用户。") - public ApiResponse restart( - @Parameter(description = "触发重启的操作人,来自内部鉴权链路透传的 X-User 请求头") - @RequestHeader(value = "X-User", required = false) String operator - ) { + public ApiResponse restart(HttpServletRequest request) { + String operator = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE); return ApiResponse.success(deviceService.restart(operator)); } } diff --git a/src/main/java/com/cisd/tms/modules/device/dto/CryptoCardRuntimeStatus.java b/src/main/java/com/cisd/tms/modules/device/dto/CryptoCardRuntimeStatus.java new file mode 100644 index 0000000..1b46e46 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/CryptoCardRuntimeStatus.java @@ -0,0 +1,29 @@ +package com.cisd.tms.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "加密卡运行状态") +public class CryptoCardRuntimeStatus { + + @Schema(description = "状态", example = "NORMAL") + private String status; + + @Schema(description = "状态说明", example = "就绪状态") + private String message; + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileDeviceSection.java b/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileDeviceSection.java new file mode 100644 index 0000000..e142ccf --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileDeviceSection.java @@ -0,0 +1,48 @@ +package com.cisd.tms.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "设备基础信息") +public class DeviceProfileDeviceSection { + + @Schema(description = "设备型号", example = "SYD7108") + private String model; + @Schema(description = "设备名称", example = "跨境支付终端一体机") + private String name; + @Schema(description = "设备序列号", example = "1234567890ABCDEF") + private String serialNumber; + @Schema(description = "设备版本", example = "V1.00") + private String deviceVersion; + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSerialNumber() { + return serialNumber; + } + + public void setSerialNumber(String serialNumber) { + this.serialNumber = serialNumber; + } + + public String getDeviceVersion() { + return deviceVersion; + } + + public void setDeviceVersion(String deviceVersion) { + this.deviceVersion = deviceVersion; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileHardwareSection.java b/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileHardwareSection.java new file mode 100644 index 0000000..84a2d9f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileHardwareSection.java @@ -0,0 +1,48 @@ +package com.cisd.tms.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "设备硬件信息") +public class DeviceProfileHardwareSection { + + @Schema(description = "密码卡名称", example = "SYD1408加密卡") + private String cryptoCardName; + @Schema(description = "CPU 型号", example = "Hygon C86-3G (OPN:3350)") + private String cpuModel; + @Schema(description = "内存总量", example = "32GB") + private String memoryTotal; + @Schema(description = "磁盘总量", example = "953.87GB") + private String diskTotal; + + public String getCryptoCardName() { + return cryptoCardName; + } + + public void setCryptoCardName(String cryptoCardName) { + this.cryptoCardName = cryptoCardName; + } + + public String getCpuModel() { + return cpuModel; + } + + public void setCpuModel(String cpuModel) { + this.cpuModel = cpuModel; + } + + public String getMemoryTotal() { + return memoryTotal; + } + + public void setMemoryTotal(String memoryTotal) { + this.memoryTotal = memoryTotal; + } + + public String getDiskTotal() { + return diskTotal; + } + + public void setDiskTotal(String diskTotal) { + this.diskTotal = diskTotal; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileResponse.java b/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileResponse.java new file mode 100644 index 0000000..5bcc483 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileResponse.java @@ -0,0 +1,40 @@ +package com.cisd.tms.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "设备画像响应") +public class DeviceProfileResponse { + + @Schema(description = "设备基础信息") + private DeviceProfileDeviceSection device; + @Schema(description = "设备硬件信息") + private DeviceProfileHardwareSection hardware; + @Schema(description = "软件版本信息") + private List software = new ArrayList<>(); + + public DeviceProfileDeviceSection getDevice() { + return device; + } + + public void setDevice(DeviceProfileDeviceSection device) { + this.device = device; + } + + public DeviceProfileHardwareSection getHardware() { + return hardware; + } + + public void setHardware(DeviceProfileHardwareSection hardware) { + this.hardware = hardware; + } + + public List getSoftware() { + return software; + } + + public void setSoftware(List software) { + this.software = software; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileSoftwareItem.java b/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileSoftwareItem.java new file mode 100644 index 0000000..205c208 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/DeviceProfileSoftwareItem.java @@ -0,0 +1,38 @@ +package com.cisd.tms.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "软件组件信息") +public class DeviceProfileSoftwareItem { + + @Schema(description = "组件编码", example = "RABBITMQ") + private String componentCode; + @Schema(description = "组件名称", example = "RabbitMQ") + private String componentName; + @Schema(description = "组件版本", example = "RabbitMQ V3.8.21") + private String version; + + public String getComponentCode() { + return componentCode; + } + + public void setComponentCode(String componentCode) { + this.componentCode = componentCode; + } + + public String getComponentName() { + return componentName; + } + + public void setComponentName(String componentName) { + this.componentName = componentName; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/DeviceRuntimeStatusResponse.java b/src/main/java/com/cisd/tms/modules/device/dto/DeviceRuntimeStatusResponse.java new file mode 100644 index 0000000..f333965 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/DeviceRuntimeStatusResponse.java @@ -0,0 +1,42 @@ +package com.cisd.tms.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "设备运行状态响应") +public class DeviceRuntimeStatusResponse { + + @Schema(description = "硬件运行状态") + private HardwareRuntimeStatusSection hardware; + + @Schema(description = "软件运行状态") + private List software = new ArrayList<>(); + + @Schema(description = "采集时间", example = "2026-03-13T14:30:00+08:00") + private String collectedAt; + + public HardwareRuntimeStatusSection getHardware() { + return hardware; + } + + public void setHardware(HardwareRuntimeStatusSection hardware) { + this.hardware = hardware; + } + + public List getSoftware() { + return software; + } + + public void setSoftware(List software) { + this.software = software; + } + + public String getCollectedAt() { + return collectedAt; + } + + public void setCollectedAt(String collectedAt) { + this.collectedAt = collectedAt; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/HardwareRuntimeStatusSection.java b/src/main/java/com/cisd/tms/modules/device/dto/HardwareRuntimeStatusSection.java new file mode 100644 index 0000000..cb3d75b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/HardwareRuntimeStatusSection.java @@ -0,0 +1,51 @@ +package com.cisd.tms.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "硬件运行状态") +public class HardwareRuntimeStatusSection { + + @Schema(description = "CPU 运行状态") + private UsageMetricStatus cpu; + + @Schema(description = "内存运行状态") + private UsageMetricStatus memory; + + @Schema(description = "磁盘运行状态") + private UsageMetricStatus disk; + + @Schema(description = "加密卡运行状态") + private CryptoCardRuntimeStatus cryptoCard; + + public UsageMetricStatus getCpu() { + return cpu; + } + + public void setCpu(UsageMetricStatus cpu) { + this.cpu = cpu; + } + + public UsageMetricStatus getMemory() { + return memory; + } + + public void setMemory(UsageMetricStatus memory) { + this.memory = memory; + } + + public UsageMetricStatus getDisk() { + return disk; + } + + public void setDisk(UsageMetricStatus disk) { + this.disk = disk; + } + + public CryptoCardRuntimeStatus getCryptoCard() { + return cryptoCard; + } + + public void setCryptoCard(CryptoCardRuntimeStatus cryptoCard) { + this.cryptoCard = cryptoCard; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/SoftwareRuntimeStatusItem.java b/src/main/java/com/cisd/tms/modules/device/dto/SoftwareRuntimeStatusItem.java new file mode 100644 index 0000000..a3aed16 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/SoftwareRuntimeStatusItem.java @@ -0,0 +1,62 @@ +package com.cisd.tms.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "软件运行状态项") +public class SoftwareRuntimeStatusItem { + + @Schema(description = "组件编码", example = "RABBITMQ") + private String componentCode; + + @Schema(description = "组件名称", example = "消息队列") + private String componentName; + + @Schema(description = "展示名称", example = "RabbitMQ V3.8.21") + private String displayName; + + @Schema(description = "状态", example = "NORMAL") + private String status; + + @Schema(description = "状态说明", example = "正常") + private String message; + + public String getComponentCode() { + return componentCode; + } + + public void setComponentCode(String componentCode) { + this.componentCode = componentCode; + } + + public String getComponentName() { + return componentName; + } + + public void setComponentName(String componentName) { + this.componentName = componentName; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/UsageMetricStatus.java b/src/main/java/com/cisd/tms/modules/device/dto/UsageMetricStatus.java new file mode 100644 index 0000000..4c04b6b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/UsageMetricStatus.java @@ -0,0 +1,62 @@ +package com.cisd.tms.modules.device.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "资源使用量状态") +public class UsageMetricStatus { + + @Schema(description = "已使用容量(GB)", example = "23.55") + private Double usedGb; + + @Schema(description = "总容量(GB)", example = "32.0") + private Double totalGb; + + @Schema(description = "使用率百分比", example = "73.59") + private Double usagePercent; + + @Schema(description = "状态", example = "NORMAL") + private String status; + + @Schema(description = "状态说明", example = "正常") + private String message; + + public Double getUsedGb() { + return usedGb; + } + + public void setUsedGb(Double usedGb) { + this.usedGb = usedGb; + } + + public Double getTotalGb() { + return totalGb; + } + + public void setTotalGb(Double totalGb) { + this.totalGb = totalGb; + } + + public Double getUsagePercent() { + return usagePercent; + } + + public void setUsagePercent(Double usagePercent) { + this.usagePercent = usagePercent; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoBackupUserKeyInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoBackupUserKeyInternalRequest.java new file mode 100644 index 0000000..9048a58 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoBackupUserKeyInternalRequest.java @@ -0,0 +1,39 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; + +public class CryptoBackupUserKeyInternalRequest { + + @Min(0) + private int keyIndex; + + @Min(0) + private int keyType; + + @Min(1) + private int outBufferSize; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getKeyType() { + return keyType; + } + + public void setKeyType(int keyType) { + this.keyType = keyType; + } + + public int getOutBufferSize() { + return outBufferSize; + } + + public void setOutBufferSize(int outBufferSize) { + this.outBufferSize = outBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoChangePrivateKeyPasswordInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoChangePrivateKeyPasswordInternalRequest.java new file mode 100644 index 0000000..f2b92c4 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoChangePrivateKeyPasswordInternalRequest.java @@ -0,0 +1,40 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoChangePrivateKeyPasswordInternalRequest { + + @Min(0) + private int keyIndex; + + @NotBlank + private String oldPasswordBase64; + + @NotBlank + private String newPasswordBase64; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public String getOldPasswordBase64() { + return oldPasswordBase64; + } + + public void setOldPasswordBase64(String oldPasswordBase64) { + this.oldPasswordBase64 = oldPasswordBase64; + } + + public String getNewPasswordBase64() { + return newPasswordBase64; + } + + public void setNewPasswordBase64(String newPasswordBase64) { + this.newPasswordBase64 = newPasswordBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoDigestInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoDigestInternalRequest.java new file mode 100644 index 0000000..955b0ad --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoDigestInternalRequest.java @@ -0,0 +1,40 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoDigestInternalRequest { + + @Min(1) + private int algId; + + @NotBlank + private String dataBase64; + + @Min(1) + private int outBufferSize = 64; + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public String getDataBase64() { + return dataBase64; + } + + public void setDataBase64(String dataBase64) { + this.dataBase64 = dataBase64; + } + + public int getOutBufferSize() { + return outBufferSize; + } + + public void setOutBufferSize(int outBufferSize) { + this.outBufferSize = outBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoExchangeEnvelopeEccInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoExchangeEnvelopeEccInternalRequest.java new file mode 100644 index 0000000..b56f14b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoExchangeEnvelopeEccInternalRequest.java @@ -0,0 +1,62 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoExchangeEnvelopeEccInternalRequest { + + @Min(0) + private int keyIndex; + + @Min(1) + private int algId; + + @NotBlank + private String publicKeyBase64; + + @NotBlank + private String inputEnvelopeBase64; + + @Min(1) + private int outBufferSize; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public String getPublicKeyBase64() { + return publicKeyBase64; + } + + public void setPublicKeyBase64(String publicKeyBase64) { + this.publicKeyBase64 = publicKeyBase64; + } + + public String getInputEnvelopeBase64() { + return inputEnvelopeBase64; + } + + public void setInputEnvelopeBase64(String inputEnvelopeBase64) { + this.inputEnvelopeBase64 = inputEnvelopeBase64; + } + + public int getOutBufferSize() { + return outBufferSize; + } + + public void setOutBufferSize(int outBufferSize) { + this.outBufferSize = outBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoExchangeEnvelopeRsaInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoExchangeEnvelopeRsaInternalRequest.java new file mode 100644 index 0000000..cd2b74b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoExchangeEnvelopeRsaInternalRequest.java @@ -0,0 +1,51 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoExchangeEnvelopeRsaInternalRequest { + + @Min(0) + private int keyIndex; + + @NotBlank + private String publicKeyBase64; + + @NotBlank + private String inputEnvelopeBase64; + + @Min(1) + private int outBufferSize; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public String getPublicKeyBase64() { + return publicKeyBase64; + } + + public void setPublicKeyBase64(String publicKeyBase64) { + this.publicKeyBase64 = publicKeyBase64; + } + + public String getInputEnvelopeBase64() { + return inputEnvelopeBase64; + } + + public void setInputEnvelopeBase64(String inputEnvelopeBase64) { + this.inputEnvelopeBase64 = inputEnvelopeBase64; + } + + public int getOutBufferSize() { + return outBufferSize; + } + + public void setOutBufferSize(int outBufferSize) { + this.outBufferSize = outBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileCreateInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileCreateInternalRequest.java new file mode 100644 index 0000000..ef6dea0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileCreateInternalRequest.java @@ -0,0 +1,29 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoFileCreateInternalRequest { + + @NotBlank + private String fileName; + + @Min(1) + private int fileSize; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public int getFileSize() { + return fileSize; + } + + public void setFileSize(int fileSize) { + this.fileSize = fileSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileDeleteInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileDeleteInternalRequest.java new file mode 100644 index 0000000..1e5b87f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileDeleteInternalRequest.java @@ -0,0 +1,17 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.NotBlank; + +public class CryptoFileDeleteInternalRequest { + + @NotBlank + private String fileName; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileReadInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileReadInternalRequest.java new file mode 100644 index 0000000..abf62f4 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileReadInternalRequest.java @@ -0,0 +1,40 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoFileReadInternalRequest { + + @NotBlank + private String fileName; + + @Min(0) + private int offset; + + @Min(1) + private int readLength; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getReadLength() { + return readLength; + } + + public void setReadLength(int readLength) { + this.readLength = readLength; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileWriteInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileWriteInternalRequest.java new file mode 100644 index 0000000..f6db422 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoFileWriteInternalRequest.java @@ -0,0 +1,40 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoFileWriteInternalRequest { + + @NotBlank + private String fileName; + + @Min(0) + private int offset; + + @NotBlank + private String dataBase64; + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public String getDataBase64() { + return dataBase64; + } + + public void setDataBase64(String dataBase64) { + this.dataBase64 = dataBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateAgreementDataAndKeyEccInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateAgreementDataAndKeyEccInternalRequest.java new file mode 100644 index 0000000..4a8431f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateAgreementDataAndKeyEccInternalRequest.java @@ -0,0 +1,95 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoGenerateAgreementDataAndKeyEccInternalRequest { + + @Min(0) + private int iskIndex; + + @Min(1) + private int keyBits; + + @NotBlank + private String responseIdBase64; + + @NotBlank + private String sponsorIdBase64; + + @NotBlank + private String sponsorPublicKeyBase64; + + @NotBlank + private String sponsorTmpPublicKeyBase64; + + @Min(1) + private int responsePublicKeyBufferSize; + + @Min(1) + private int responseTmpPublicKeyBufferSize; + + public int getIskIndex() { + return iskIndex; + } + + public void setIskIndex(int iskIndex) { + this.iskIndex = iskIndex; + } + + public int getKeyBits() { + return keyBits; + } + + public void setKeyBits(int keyBits) { + this.keyBits = keyBits; + } + + public String getResponseIdBase64() { + return responseIdBase64; + } + + public void setResponseIdBase64(String responseIdBase64) { + this.responseIdBase64 = responseIdBase64; + } + + public String getSponsorIdBase64() { + return sponsorIdBase64; + } + + public void setSponsorIdBase64(String sponsorIdBase64) { + this.sponsorIdBase64 = sponsorIdBase64; + } + + public String getSponsorPublicKeyBase64() { + return sponsorPublicKeyBase64; + } + + public void setSponsorPublicKeyBase64(String sponsorPublicKeyBase64) { + this.sponsorPublicKeyBase64 = sponsorPublicKeyBase64; + } + + public String getSponsorTmpPublicKeyBase64() { + return sponsorTmpPublicKeyBase64; + } + + public void setSponsorTmpPublicKeyBase64(String sponsorTmpPublicKeyBase64) { + this.sponsorTmpPublicKeyBase64 = sponsorTmpPublicKeyBase64; + } + + public int getResponsePublicKeyBufferSize() { + return responsePublicKeyBufferSize; + } + + public void setResponsePublicKeyBufferSize(int responsePublicKeyBufferSize) { + this.responsePublicKeyBufferSize = responsePublicKeyBufferSize; + } + + public int getResponseTmpPublicKeyBufferSize() { + return responseTmpPublicKeyBufferSize; + } + + public void setResponseTmpPublicKeyBufferSize(int responseTmpPublicKeyBufferSize) { + this.responseTmpPublicKeyBufferSize = responseTmpPublicKeyBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateKekInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateKekInternalRequest.java new file mode 100644 index 0000000..10b4c01 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateKekInternalRequest.java @@ -0,0 +1,28 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; + +public class CryptoGenerateKekInternalRequest { + + @Min(1) + private int keyBits; + + @Min(0) + private int keyIndex; + + public int getKeyBits() { + return keyBits; + } + + public void setKeyBits(int keyBits) { + this.keyBits = keyBits; + } + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateKeyPairInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateKeyPairInternalRequest.java new file mode 100644 index 0000000..f8258f3 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateKeyPairInternalRequest.java @@ -0,0 +1,39 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; + +public class CryptoGenerateKeyPairInternalRequest { + + @Min(1) + private int keyBits; + + @Min(0) + private int keyIndex; + + @Min(0) + private int keyType; + + public int getKeyBits() { + return keyBits; + } + + public void setKeyBits(int keyBits) { + this.keyBits = keyBits; + } + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getKeyType() { + return keyType; + } + + public void setKeyType(int keyType) { + this.keyType = keyType; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateSessionKeyEccInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateSessionKeyEccInternalRequest.java new file mode 100644 index 0000000..f9efe68 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateSessionKeyEccInternalRequest.java @@ -0,0 +1,106 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoGenerateSessionKeyEccInternalRequest { + + @Min(0) + private int iskIndex; + + @Min(1) + private int keyBits; + + @NotBlank + private String sponsorIdBase64; + + @NotBlank + private String responseIdBase64; + + @NotBlank + private String responsePublicKeyBase64; + + @NotBlank + private String responseTmpPublicKeyBase64; + + @Min(1) + private int sponsorPublicKeyBufferSize; + + @Min(1) + private int sponsorTmpPublicKeyBufferSize; + + @Min(1) + private int sessionKeyBufferSize; + + public int getIskIndex() { + return iskIndex; + } + + public void setIskIndex(int iskIndex) { + this.iskIndex = iskIndex; + } + + public int getKeyBits() { + return keyBits; + } + + public void setKeyBits(int keyBits) { + this.keyBits = keyBits; + } + + public String getSponsorIdBase64() { + return sponsorIdBase64; + } + + public void setSponsorIdBase64(String sponsorIdBase64) { + this.sponsorIdBase64 = sponsorIdBase64; + } + + public String getResponseIdBase64() { + return responseIdBase64; + } + + public void setResponseIdBase64(String responseIdBase64) { + this.responseIdBase64 = responseIdBase64; + } + + public String getResponsePublicKeyBase64() { + return responsePublicKeyBase64; + } + + public void setResponsePublicKeyBase64(String responsePublicKeyBase64) { + this.responsePublicKeyBase64 = responsePublicKeyBase64; + } + + public String getResponseTmpPublicKeyBase64() { + return responseTmpPublicKeyBase64; + } + + public void setResponseTmpPublicKeyBase64(String responseTmpPublicKeyBase64) { + this.responseTmpPublicKeyBase64 = responseTmpPublicKeyBase64; + } + + public int getSponsorPublicKeyBufferSize() { + return sponsorPublicKeyBufferSize; + } + + public void setSponsorPublicKeyBufferSize(int sponsorPublicKeyBufferSize) { + this.sponsorPublicKeyBufferSize = sponsorPublicKeyBufferSize; + } + + public int getSponsorTmpPublicKeyBufferSize() { + return sponsorTmpPublicKeyBufferSize; + } + + public void setSponsorTmpPublicKeyBufferSize(int sponsorTmpPublicKeyBufferSize) { + this.sponsorTmpPublicKeyBufferSize = sponsorTmpPublicKeyBufferSize; + } + + public int getSessionKeyBufferSize() { + return sessionKeyBufferSize; + } + + public void setSessionKeyBufferSize(int sessionKeyBufferSize) { + this.sessionKeyBufferSize = sessionKeyBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateStandardEccKeyPairInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateStandardEccKeyPairInternalRequest.java new file mode 100644 index 0000000..97a4b8c --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateStandardEccKeyPairInternalRequest.java @@ -0,0 +1,50 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; + +public class CryptoGenerateStandardEccKeyPairInternalRequest { + + @Min(1) + private int algId; + + @Min(1) + private int keyBits; + + @Min(1) + private int publicKeyBufferSize; + + @Min(1) + private int privateKeyBufferSize; + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public int getKeyBits() { + return keyBits; + } + + public void setKeyBits(int keyBits) { + this.keyBits = keyBits; + } + + public int getPublicKeyBufferSize() { + return publicKeyBufferSize; + } + + public void setPublicKeyBufferSize(int publicKeyBufferSize) { + this.publicKeyBufferSize = publicKeyBufferSize; + } + + public int getPrivateKeyBufferSize() { + return privateKeyBufferSize; + } + + public void setPrivateKeyBufferSize(int privateKeyBufferSize) { + this.privateKeyBufferSize = privateKeyBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateStandardRsaKeyPairInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateStandardRsaKeyPairInternalRequest.java new file mode 100644 index 0000000..a9d05e7 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoGenerateStandardRsaKeyPairInternalRequest.java @@ -0,0 +1,39 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; + +public class CryptoGenerateStandardRsaKeyPairInternalRequest { + + @Min(1) + private int keyBits; + + @Min(1) + private int publicKeyBufferSize; + + @Min(1) + private int privateKeyBufferSize; + + public int getKeyBits() { + return keyBits; + } + + public void setKeyBits(int keyBits) { + this.keyBits = keyBits; + } + + public int getPublicKeyBufferSize() { + return publicKeyBufferSize; + } + + public void setPublicKeyBufferSize(int publicKeyBufferSize) { + this.publicKeyBufferSize = publicKeyBufferSize; + } + + public int getPrivateKeyBufferSize() { + return privateKeyBufferSize; + } + + public void setPrivateKeyBufferSize(int privateKeyBufferSize) { + this.privateKeyBufferSize = privateKeyBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoHmacInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoHmacInternalRequest.java new file mode 100644 index 0000000..6b0ee76 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoHmacInternalRequest.java @@ -0,0 +1,40 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoHmacInternalRequest { + + @Min(1) + private int algId; + + @NotBlank + private String keyBase64; + + @NotBlank + private String dataBase64; + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public String getKeyBase64() { + return keyBase64; + } + + public void setKeyBase64(String keyBase64) { + this.keyBase64 = keyBase64; + } + + public String getDataBase64() { + return dataBase64; + } + + public void setDataBase64(String dataBase64) { + this.dataBase64 = dataBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportEncryptedKeyPairEccInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportEncryptedKeyPairEccInternalRequest.java new file mode 100644 index 0000000..a99a977 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportEncryptedKeyPairEccInternalRequest.java @@ -0,0 +1,29 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoImportEncryptedKeyPairEccInternalRequest { + + @Min(0) + private int keyIndex; + + @NotBlank + private String envelopedKeyBlobBase64; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public String getEnvelopedKeyBlobBase64() { + return envelopedKeyBlobBase64; + } + + public void setEnvelopedKeyBlobBase64(String envelopedKeyBlobBase64) { + this.envelopedKeyBlobBase64 = envelopedKeyBlobBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportEncryptedKeyPairRsaInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportEncryptedKeyPairRsaInternalRequest.java new file mode 100644 index 0000000..b38aa4f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportEncryptedKeyPairRsaInternalRequest.java @@ -0,0 +1,51 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoImportEncryptedKeyPairRsaInternalRequest { + + @Min(0) + private int keyIndex; + + @Min(1) + private int symAlgId; + + @NotBlank + private String wrappedKeyBase64; + + @NotBlank + private String encryptedDataBase64; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getSymAlgId() { + return symAlgId; + } + + public void setSymAlgId(int symAlgId) { + this.symAlgId = symAlgId; + } + + public String getWrappedKeyBase64() { + return wrappedKeyBase64; + } + + public void setWrappedKeyBase64(String wrappedKeyBase64) { + this.wrappedKeyBase64 = wrappedKeyBase64; + } + + public String getEncryptedDataBase64() { + return encryptedDataBase64; + } + + public void setEncryptedDataBase64(String encryptedDataBase64) { + this.encryptedDataBase64 = encryptedDataBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportKekInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportKekInternalRequest.java new file mode 100644 index 0000000..f1734d0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportKekInternalRequest.java @@ -0,0 +1,29 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoImportKekInternalRequest { + + @Min(0) + private int keyIndex; + + @NotBlank + private String keyDataBase64; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public String getKeyDataBase64() { + return keyDataBase64; + } + + public void setKeyDataBase64(String keyDataBase64) { + this.keyDataBase64 = keyDataBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportKeyPairInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportKeyPairInternalRequest.java new file mode 100644 index 0000000..9580914 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoImportKeyPairInternalRequest.java @@ -0,0 +1,51 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoImportKeyPairInternalRequest { + + @Min(0) + private int keyIndex; + + @Min(0) + private int keyType; + + @NotBlank + private String publicKeyBase64; + + @NotBlank + private String privateKeyBase64; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getKeyType() { + return keyType; + } + + public void setKeyType(int keyType) { + this.keyType = keyType; + } + + public String getPublicKeyBase64() { + return publicKeyBase64; + } + + public void setPublicKeyBase64(String publicKeyBase64) { + this.publicKeyBase64 = publicKeyBase64; + } + + public String getPrivateKeyBase64() { + return privateKeyBase64; + } + + public void setPrivateKeyBase64(String privateKeyBase64) { + this.privateKeyBase64 = privateKeyBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoInitIdentifyInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoInitIdentifyInternalRequest.java new file mode 100644 index 0000000..dc0204a --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoInitIdentifyInternalRequest.java @@ -0,0 +1,28 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.NotBlank; + +public class CryptoInitIdentifyInternalRequest { + + @NotBlank + private String oldPinBase64; + + @NotBlank + private String newPinBase64; + + public String getOldPinBase64() { + return oldPinBase64; + } + + public void setOldPinBase64(String oldPinBase64) { + this.oldPinBase64 = oldPinBase64; + } + + public String getNewPinBase64() { + return newPinBase64; + } + + public void setNewPinBase64(String newPinBase64) { + this.newPinBase64 = newPinBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoKekComponentExportInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoKekComponentExportInternalRequest.java new file mode 100644 index 0000000..524fb4e --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoKekComponentExportInternalRequest.java @@ -0,0 +1,39 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; + +public class CryptoKekComponentExportInternalRequest { + + @Min(0) + private int keyIndex; + + @Min(0) + private int componentNum; + + @Min(1) + private int componentSize; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getComponentNum() { + return componentNum; + } + + public void setComponentNum(int componentNum) { + this.componentNum = componentNum; + } + + public int getComponentSize() { + return componentSize; + } + + public void setComponentSize(int componentSize) { + this.componentSize = componentSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoKekComponentImportInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoKekComponentImportInternalRequest.java new file mode 100644 index 0000000..d98938d --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoKekComponentImportInternalRequest.java @@ -0,0 +1,40 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoKekComponentImportInternalRequest { + + @Min(0) + private int keyIndex; + + @Min(0) + private int componentNum; + + @NotBlank + private String componentDataBase64; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getComponentNum() { + return componentNum; + } + + public void setComponentNum(int componentNum) { + this.componentNum = componentNum; + } + + public String getComponentDataBase64() { + return componentDataBase64; + } + + public void setComponentDataBase64(String componentDataBase64) { + this.componentDataBase64 = componentDataBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoMacInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoMacInternalRequest.java new file mode 100644 index 0000000..6f7bac0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoMacInternalRequest.java @@ -0,0 +1,61 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoMacInternalRequest { + + @Min(1) + private int algId; + + @NotBlank + private String keyBase64; + + private String ivBase64; + + @NotBlank + private String dataBase64; + + @Min(1) + private int outBufferSize = 32; + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public String getKeyBase64() { + return keyBase64; + } + + public void setKeyBase64(String keyBase64) { + this.keyBase64 = keyBase64; + } + + public String getIvBase64() { + return ivBase64; + } + + public void setIvBase64(String ivBase64) { + this.ivBase64 = ivBase64; + } + + public String getDataBase64() { + return dataBase64; + } + + public void setDataBase64(String dataBase64) { + this.dataBase64 = dataBase64; + } + + public int getOutBufferSize() { + return outBufferSize; + } + + public void setOutBufferSize(int outBufferSize) { + this.outBufferSize = outBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoPrivateKeyAccessInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoPrivateKeyAccessInternalRequest.java new file mode 100644 index 0000000..5777c3e --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoPrivateKeyAccessInternalRequest.java @@ -0,0 +1,29 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoPrivateKeyAccessInternalRequest { + + @Min(0) + private int keyIndex; + + @NotBlank + private String passwordBase64; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public String getPasswordBase64() { + return passwordBase64; + } + + public void setPasswordBase64(String passwordBase64) { + this.passwordBase64 = passwordBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoReadUkeyIkComponentInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoReadUkeyIkComponentInternalRequest.java new file mode 100644 index 0000000..ea6f41f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoReadUkeyIkComponentInternalRequest.java @@ -0,0 +1,29 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoReadUkeyIkComponentInternalRequest { + + @Min(0) + private int keyType; + + @NotBlank + private String ukeyPinBase64; + + public int getKeyType() { + return keyType; + } + + public void setKeyType(int keyType) { + this.keyType = keyType; + } + + public String getUkeyPinBase64() { + return ukeyPinBase64; + } + + public void setUkeyPinBase64(String ukeyPinBase64) { + this.ukeyPinBase64 = ukeyPinBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRecoverKekInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRecoverKekInternalRequest.java new file mode 100644 index 0000000..94efa65 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRecoverKekInternalRequest.java @@ -0,0 +1,29 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoRecoverKekInternalRequest { + + @Min(0) + private int keyIndex; + + @NotBlank + private String encryptedKeyBase64; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public String getEncryptedKeyBase64() { + return encryptedKeyBase64; + } + + public void setEncryptedKeyBase64(String encryptedKeyBase64) { + this.encryptedKeyBase64 = encryptedKeyBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRecoverLmkExInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRecoverLmkExInternalRequest.java new file mode 100644 index 0000000..06e53a7 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRecoverLmkExInternalRequest.java @@ -0,0 +1,34 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import java.util.Map; + +public class CryptoRecoverLmkExInternalRequest { + + private String componentBase64; + private String lmkMacHex; + private Map components; + + public String getComponentBase64() { + return componentBase64; + } + + public void setComponentBase64(String componentBase64) { + this.componentBase64 = componentBase64; + } + + public String getLmkMacHex() { + return lmkMacHex; + } + + public void setLmkMacHex(String lmkMacHex) { + this.lmkMacHex = lmkMacHex; + } + + public Map getComponents() { + return components; + } + + public void setComponents(Map components) { + this.components = components; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRecoverUserKeyInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRecoverUserKeyInternalRequest.java new file mode 100644 index 0000000..89510ae --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRecoverUserKeyInternalRequest.java @@ -0,0 +1,51 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoRecoverUserKeyInternalRequest { + + @Min(0) + private int keyIndex; + + @Min(0) + private int keyType; + + @NotBlank + private String encryptedKeyBase64; + + @Min(0) + private int storeFlag = 1; + + public int getKeyIndex() { + return keyIndex; + } + + public void setKeyIndex(int keyIndex) { + this.keyIndex = keyIndex; + } + + public int getKeyType() { + return keyType; + } + + public void setKeyType(int keyType) { + this.keyType = keyType; + } + + public String getEncryptedKeyBase64() { + return encryptedKeyBase64; + } + + public void setEncryptedKeyBase64(String encryptedKeyBase64) { + this.encryptedKeyBase64 = encryptedKeyBase64; + } + + public int getStoreFlag() { + return storeFlag; + } + + public void setStoreFlag(int storeFlag) { + this.storeFlag = storeFlag; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRestoreAllKeysInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRestoreAllKeysInternalRequest.java new file mode 100644 index 0000000..c58ba76 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoRestoreAllKeysInternalRequest.java @@ -0,0 +1,17 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.NotBlank; + +public class CryptoRestoreAllKeysInternalRequest { + + @NotBlank + private String keyImageBase64; + + public String getKeyImageBase64() { + return keyImageBase64; + } + + public void setKeyImageBase64(String keyImageBase64) { + this.keyImageBase64 = keyImageBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoSymmetricInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoSymmetricInternalRequest.java new file mode 100644 index 0000000..4b87563 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoSymmetricInternalRequest.java @@ -0,0 +1,61 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoSymmetricInternalRequest { + + @Min(1) + private int algId; + + @NotBlank + private String keyBase64; + + private String ivBase64; + + @NotBlank + private String dataBase64; + + @Min(1) + private int outBufferSize; + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public String getKeyBase64() { + return keyBase64; + } + + public void setKeyBase64(String keyBase64) { + this.keyBase64 = keyBase64; + } + + public String getIvBase64() { + return ivBase64; + } + + public void setIvBase64(String ivBase64) { + this.ivBase64 = ivBase64; + } + + public String getDataBase64() { + return dataBase64; + } + + public void setDataBase64(String dataBase64) { + this.dataBase64 = dataBase64; + } + + public int getOutBufferSize() { + return outBufferSize; + } + + public void setOutBufferSize(int outBufferSize) { + this.outBufferSize = outBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoSymmetricKekInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoSymmetricKekInternalRequest.java new file mode 100644 index 0000000..57e2fa7 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoSymmetricKekInternalRequest.java @@ -0,0 +1,72 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoSymmetricKekInternalRequest { + + @Min(1) + private int algId; + + @Min(0) + private int kekIndex; + + @NotBlank + private String wrappedKeyBase64; + + private String ivBase64; + + @NotBlank + private String dataBase64; + + @Min(1) + private int outBufferSize; + + public int getAlgId() { + return algId; + } + + public void setAlgId(int algId) { + this.algId = algId; + } + + public int getKekIndex() { + return kekIndex; + } + + public void setKekIndex(int kekIndex) { + this.kekIndex = kekIndex; + } + + public String getWrappedKeyBase64() { + return wrappedKeyBase64; + } + + public void setWrappedKeyBase64(String wrappedKeyBase64) { + this.wrappedKeyBase64 = wrappedKeyBase64; + } + + public String getIvBase64() { + return ivBase64; + } + + public void setIvBase64(String ivBase64) { + this.ivBase64 = ivBase64; + } + + public String getDataBase64() { + return dataBase64; + } + + public void setDataBase64(String dataBase64) { + this.dataBase64 = dataBase64; + } + + public int getOutBufferSize() { + return outBufferSize; + } + + public void setOutBufferSize(int outBufferSize) { + this.outBufferSize = outBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyGenerateInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyGenerateInternalRequest.java new file mode 100644 index 0000000..2b210d0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyGenerateInternalRequest.java @@ -0,0 +1,40 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoUkeyGenerateInternalRequest { + + @Min(0) + private int adminIndex; + + @NotBlank + private String ukeyPinBase64; + + @NotBlank + private String loginPinBase64; + + public int getAdminIndex() { + return adminIndex; + } + + public void setAdminIndex(int adminIndex) { + this.adminIndex = adminIndex; + } + + public String getUkeyPinBase64() { + return ukeyPinBase64; + } + + public void setUkeyPinBase64(String ukeyPinBase64) { + this.ukeyPinBase64 = ukeyPinBase64; + } + + public String getLoginPinBase64() { + return loginPinBase64; + } + + public void setLoginPinBase64(String loginPinBase64) { + this.loginPinBase64 = loginPinBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyLoginInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyLoginInternalRequest.java new file mode 100644 index 0000000..0889a07 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyLoginInternalRequest.java @@ -0,0 +1,28 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.NotBlank; + +public class CryptoUkeyLoginInternalRequest { + + @NotBlank + private String ukeyPinBase64; + + @NotBlank + private String loginPinBase64; + + public String getUkeyPinBase64() { + return ukeyPinBase64; + } + + public void setUkeyPinBase64(String ukeyPinBase64) { + this.ukeyPinBase64 = ukeyPinBase64; + } + + public String getLoginPinBase64() { + return loginPinBase64; + } + + public void setLoginPinBase64(String loginPinBase64) { + this.loginPinBase64 = loginPinBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyPinChangeInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyPinChangeInternalRequest.java new file mode 100644 index 0000000..748f7f0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyPinChangeInternalRequest.java @@ -0,0 +1,40 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoUkeyPinChangeInternalRequest { + + @Min(0) + private int pinType; + + @NotBlank + private String oldPinBase64; + + @NotBlank + private String newPinBase64; + + public int getPinType() { + return pinType; + } + + public void setPinType(int pinType) { + this.pinType = pinType; + } + + public String getOldPinBase64() { + return oldPinBase64; + } + + public void setOldPinBase64(String oldPinBase64) { + this.oldPinBase64 = oldPinBase64; + } + + public String getNewPinBase64() { + return newPinBase64; + } + + public void setNewPinBase64(String newPinBase64) { + this.newPinBase64 = newPinBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyPinInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyPinInternalRequest.java new file mode 100644 index 0000000..f81fb78 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyPinInternalRequest.java @@ -0,0 +1,17 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.NotBlank; + +public class CryptoUkeyPinInternalRequest { + + @NotBlank + private String ukeyPinBase64; + + public String getUkeyPinBase64() { + return ukeyPinBase64; + } + + public void setUkeyPinBase64(String ukeyPinBase64) { + this.ukeyPinBase64 = ukeyPinBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyUnlockInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyUnlockInternalRequest.java new file mode 100644 index 0000000..80878f0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUkeyUnlockInternalRequest.java @@ -0,0 +1,28 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.NotBlank; + +public class CryptoUkeyUnlockInternalRequest { + + @NotBlank + private String adminPinBase64; + + @NotBlank + private String userPinBase64; + + public String getAdminPinBase64() { + return adminPinBase64; + } + + public void setAdminPinBase64(String adminPinBase64) { + this.adminPinBase64 = adminPinBase64; + } + + public String getUserPinBase64() { + return userPinBase64; + } + + public void setUserPinBase64(String userPinBase64) { + this.userPinBase64 = userPinBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUpdateBackedKeyInternalRequest.java b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUpdateBackedKeyInternalRequest.java new file mode 100644 index 0000000..e5e60ba --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/dto/crypto/request/CryptoUpdateBackedKeyInternalRequest.java @@ -0,0 +1,29 @@ +package com.cisd.tms.modules.device.dto.crypto.request; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; + +public class CryptoUpdateBackedKeyInternalRequest { + + @NotBlank + private String oldDataBase64; + + @Min(1) + private int newDataBufferSize; + + public String getOldDataBase64() { + return oldDataBase64; + } + + public void setOldDataBase64(String oldDataBase64) { + this.oldDataBase64 = oldDataBase64; + } + + public int getNewDataBufferSize() { + return newDataBufferSize; + } + + public void setNewDataBufferSize(int newDataBufferSize) { + this.newDataBufferSize = newDataBufferSize; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/entity/DeviceSoftwareVersionEntity.java b/src/main/java/com/cisd/tms/modules/device/entity/DeviceSoftwareVersionEntity.java new file mode 100644 index 0000000..f335946 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/entity/DeviceSoftwareVersionEntity.java @@ -0,0 +1,64 @@ +package com.cisd.tms.modules.device.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.cisd.tms.infrastructure.persistence.entity.BaseEntity; +import java.time.LocalDateTime; + +@TableName("tms_device_software_version") +public class DeviceSoftwareVersionEntity extends BaseEntity { + + private String componentCode; + private String componentName; + private String currentVersion; + private String sourceType; + private LocalDateTime detectedAt; + private String remarks; + + public String getComponentCode() { + return componentCode; + } + + public void setComponentCode(String componentCode) { + this.componentCode = componentCode; + } + + public String getComponentName() { + return componentName; + } + + public void setComponentName(String componentName) { + this.componentName = componentName; + } + + public String getCurrentVersion() { + return currentVersion; + } + + public void setCurrentVersion(String currentVersion) { + this.currentVersion = currentVersion; + } + + public String getSourceType() { + return sourceType; + } + + public void setSourceType(String sourceType) { + this.sourceType = sourceType; + } + + public LocalDateTime getDetectedAt() { + return detectedAt; + } + + public void setDetectedAt(LocalDateTime detectedAt) { + this.detectedAt = detectedAt; + } + + public String getRemarks() { + return remarks; + } + + public void setRemarks(String remarks) { + this.remarks = remarks; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/mapper/DeviceSoftwareVersionMapper.java b/src/main/java/com/cisd/tms/modules/device/mapper/DeviceSoftwareVersionMapper.java new file mode 100644 index 0000000..136ed05 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/mapper/DeviceSoftwareVersionMapper.java @@ -0,0 +1,14 @@ +package com.cisd.tms.modules.device.mapper; + +import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface DeviceSoftwareVersionMapper extends BaseMapperX { + + DeviceSoftwareVersionEntity selectByComponentCode(@Param("componentCode") String componentCode); + + int updateByComponentCode(DeviceSoftwareVersionEntity entity); +} diff --git a/src/main/java/com/cisd/tms/modules/device/repository/DeviceSoftwareVersionRepository.java b/src/main/java/com/cisd/tms/modules/device/repository/DeviceSoftwareVersionRepository.java new file mode 100644 index 0000000..4fdcc89 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/repository/DeviceSoftwareVersionRepository.java @@ -0,0 +1,11 @@ +package com.cisd.tms.modules.device.repository; + +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import java.util.Optional; + +public interface DeviceSoftwareVersionRepository { + + Optional findByComponentCode(String componentCode); + + void saveOrUpdate(DeviceSoftwareVersionEntity entity); +} diff --git a/src/main/java/com/cisd/tms/modules/device/repository/impl/DeviceSoftwareVersionRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/device/repository/impl/DeviceSoftwareVersionRepositoryImpl.java new file mode 100644 index 0000000..479ae6b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/repository/impl/DeviceSoftwareVersionRepositoryImpl.java @@ -0,0 +1,31 @@ +package com.cisd.tms.modules.device.repository.impl; + +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.mapper.DeviceSoftwareVersionMapper; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public class DeviceSoftwareVersionRepositoryImpl implements DeviceSoftwareVersionRepository { + + private final DeviceSoftwareVersionMapper deviceSoftwareVersionMapper; + + public DeviceSoftwareVersionRepositoryImpl(DeviceSoftwareVersionMapper deviceSoftwareVersionMapper) { + this.deviceSoftwareVersionMapper = deviceSoftwareVersionMapper; + } + + @Override + public Optional findByComponentCode(String componentCode) { + return Optional.ofNullable(deviceSoftwareVersionMapper.selectByComponentCode(componentCode)); + } + + @Override + public void saveOrUpdate(DeviceSoftwareVersionEntity entity) { + if (findByComponentCode(entity.getComponentCode()).isPresent()) { + deviceSoftwareVersionMapper.updateByComponentCode(entity); + return; + } + deviceSoftwareVersionMapper.insert(entity); + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/service/DeviceProfileService.java b/src/main/java/com/cisd/tms/modules/device/service/DeviceProfileService.java new file mode 100644 index 0000000..ec33a92 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/service/DeviceProfileService.java @@ -0,0 +1,178 @@ +package com.cisd.tms.modules.device.service; + +import com.cisd.tms.common.config.properties.CisdPresetProperties; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.device.config.DeviceProfileProperties; +import com.cisd.tms.modules.device.dto.DeviceProfileDeviceSection; +import com.cisd.tms.modules.device.dto.DeviceProfileHardwareSection; +import com.cisd.tms.modules.device.dto.DeviceProfileResponse; +import com.cisd.tms.modules.device.dto.DeviceProfileSoftwareItem; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.device.support.SystemHardwareProbe; +import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe; +import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; +import com.cisd.tms.modules.init.service.InitService; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.springframework.stereotype.Service; + +@Service +public class DeviceProfileService { + + private final InitService initService; + private final PcieCryptoService pcieCryptoService; + private final DeviceProfileProperties deviceProfileProperties; + private final SystemHardwareProbe systemHardwareProbe; + private final SystemSoftwareVersionProbe systemSoftwareVersionProbe; + private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository; + private final CisdPresetProperties cisdPresetProperties; + + public DeviceProfileService( + InitService initService, + PcieCryptoService pcieCryptoService, + DeviceProfileProperties deviceProfileProperties, + SystemHardwareProbe systemHardwareProbe, + SystemSoftwareVersionProbe systemSoftwareVersionProbe, + DeviceSoftwareVersionRepository deviceSoftwareVersionRepository, + CisdPresetProperties cisdPresetProperties + ) { + this.initService = initService; + this.pcieCryptoService = pcieCryptoService; + this.deviceProfileProperties = deviceProfileProperties; + this.systemHardwareProbe = systemHardwareProbe; + this.systemSoftwareVersionProbe = systemSoftwareVersionProbe; + this.deviceSoftwareVersionRepository = deviceSoftwareVersionRepository; + this.cisdPresetProperties = cisdPresetProperties; + } + + public DeviceProfileResponse profile() { + DeviceInfoResult deviceInfo = safeDeviceInfo(); + DeviceProfileResponse response = new DeviceProfileResponse(); + response.setDevice(buildDeviceSection(deviceInfo)); + response.setHardware(buildHardwareSection(deviceInfo)); + response.setSoftware(buildSoftwareItems()); + return response; + } + + private DeviceProfileDeviceSection buildDeviceSection(DeviceInfoResult info) { + DeviceProfileDeviceSection section = new DeviceProfileDeviceSection(); + section.setModel(deviceProfileProperties.getModel()); + section.setName(deviceProfileProperties.getName()); + section.setSerialNumber(trim(info.getDeviceSerial())); + section.setDeviceVersion(formatVersion(info.getStandardVersion())); + return section; + } + + private DeviceProfileHardwareSection buildHardwareSection(DeviceInfoResult info) { + DeviceProfileHardwareSection section = new DeviceProfileHardwareSection(); + section.setCryptoCardName(trim(info.getDeviceName())); + section.setCpuModel(firstNonBlank(systemHardwareProbe.cpuModel(), deviceProfileProperties.getCpuModelDefault())); + section.setMemoryTotal(firstNonBlank(systemHardwareProbe.memoryTotal(), deviceProfileProperties.getMemoryTotalDefault())); + section.setDiskTotal(firstNonBlank(systemHardwareProbe.diskTotal(), deviceProfileProperties.getDiskTotalDefault())); + return section; + } + + private List buildSoftwareItems() { + List items = new ArrayList<>(); + addSoftwareItem(items, "OS", "操作系统", firstNonBlank(systemSoftwareVersionProbe.osVersion(), cached("OS"))); + addSoftwareItem(items, "TMS", "设备管理软件", firstNonBlank(cached("TMS"), systemSoftwareVersionProbe.tmsVersion(), cisdPresetProperties.getVersion())); + + CurrentInitConfigResponse currentConfig = loadCurrentInitConfigIfPresent(); + if (currentConfig == null) { + return items; + } + + String productType = trim(currentConfig.getProductType()); + String mqType = trim(currentConfig.getMqType()); + if ("RABBITMQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { + addSoftwareItem(items, "RABBITMQ", "消息队列", cached("RABBITMQ")); + } + if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { + addSoftwareItem(items, "TLQ", "消息队列", cached("TLQ")); + } + if ("ENTERPRISE".equals(productType) || "INDIRECT".equals(productType)) { + addSoftwareItem(items, "TIDB", "数据库", cached("TIDB")); + addSoftwareItem(items, "NGINX", "Nginx", cached("NGINX")); + addSoftwareItem(items, "APP", appName(productType), firstNonBlank(cached("APP"), cisdPresetProperties.getVersion())); + } else if ("DIRECT".equals(productType)) { + addSoftwareItem(items, "APP", appName(productType), firstNonBlank(cached("APP"), cisdPresetProperties.getVersion())); + } + return items; + } + + private CurrentInitConfigResponse loadCurrentInitConfigIfPresent() { + try { + if (!"INITIALIZED".equals(initService.getDeviceInitStateSnapshot().initState())) { + return null; + } + return initService.loadCurrentInitConfig(); + } catch (BizException ex) { + return null; + } + } + + private DeviceInfoResult safeDeviceInfo() { + try { + DeviceInfoResult result = pcieCryptoService.getDeviceInfo(); + return result == null ? new DeviceInfoResult() : result; + } catch (RuntimeException ex) { + return new DeviceInfoResult(); + } + } + + private void addSoftwareItem(List items, String code, String name, String version) { + DeviceProfileSoftwareItem item = new DeviceProfileSoftwareItem(); + item.setComponentCode(code); + item.setComponentName(name); + item.setVersion(trim(version)); + items.add(item); + } + + private String cached(String componentCode) { + Optional cached = deviceSoftwareVersionRepository.findByComponentCode(componentCode); + return cached.map(DeviceSoftwareVersionEntity::getCurrentVersion).orElse(""); + } + + private String appName(String productType) { + return switch (productType) { + case "ENTERPRISE" -> "标准收发器企业版"; + case "INDIRECT" -> "标准收发器间参版"; + case "DIRECT" -> "直参轻量化终端版"; + default -> "应用软件"; + }; + } + + private String formatVersion(int version) { + if (version <= 0) { + return ""; + } + if (version >= 100) { + return "V" + (version / 100) + "." + String.format("%02d", version % 100); + } + return "V" + version; + } + + private static String firstNonBlank(String... values) { + if (values == null) { + return ""; + } + for (String value : values) { + if (!isBlank(value)) { + return trim(value); + } + } + return ""; + } + + private static String trim(String value) { + return value == null ? "" : value.trim(); + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusService.java b/src/main/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusService.java new file mode 100644 index 0000000..6064248 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusService.java @@ -0,0 +1,239 @@ +package com.cisd.tms.modules.device.service; + +import com.cisd.tms.common.config.properties.CisdPresetProperties; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.device.dto.CryptoCardRuntimeStatus; +import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse; +import com.cisd.tms.modules.device.dto.HardwareRuntimeStatusSection; +import com.cisd.tms.modules.device.dto.SoftwareRuntimeStatusItem; +import com.cisd.tms.modules.device.dto.UsageMetricStatus; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.device.support.HardwareStatusProbe; +import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe; +import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe; +import com.cisd.tms.modules.device.support.UsageSnapshot; +import com.cisd.tms.modules.init.dto.CurrentInitConfigResponse; +import com.cisd.tms.modules.init.service.InitService; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.BooleanSupplier; +import org.springframework.stereotype.Service; + +@Service +public class DeviceRuntimeStatusService { + + private static final String STATUS_NORMAL = "NORMAL"; + private static final String STATUS_ABNORMAL = "ABNORMAL"; + private static final String STATUS_NOT_INSTALLED = "NOT_INSTALLED"; + private static final String STATUS_UNKNOWN = "UNKNOWN"; + + private final InitService initService; + private final PcieCryptoService pcieCryptoService; + private final HardwareStatusProbe hardwareStatusProbe; + private final SystemSoftwareVersionProbe systemSoftwareVersionProbe; + private final SoftwareRuntimeProbe softwareRuntimeProbe; + private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository; + private final CisdPresetProperties cisdPresetProperties; + + public DeviceRuntimeStatusService( + InitService initService, + PcieCryptoService pcieCryptoService, + HardwareStatusProbe hardwareStatusProbe, + SystemSoftwareVersionProbe systemSoftwareVersionProbe, + SoftwareRuntimeProbe softwareRuntimeProbe, + DeviceSoftwareVersionRepository deviceSoftwareVersionRepository, + CisdPresetProperties cisdPresetProperties + ) { + this.initService = initService; + this.pcieCryptoService = pcieCryptoService; + this.hardwareStatusProbe = hardwareStatusProbe; + this.systemSoftwareVersionProbe = systemSoftwareVersionProbe; + this.softwareRuntimeProbe = softwareRuntimeProbe; + this.deviceSoftwareVersionRepository = deviceSoftwareVersionRepository; + this.cisdPresetProperties = cisdPresetProperties; + } + + public DeviceRuntimeStatusResponse loadRuntimeStatus() { + DeviceRuntimeStatusResponse response = new DeviceRuntimeStatusResponse(); + response.setHardware(buildHardwareStatus()); + response.setSoftware(buildSoftwareStatuses()); + response.setCollectedAt(OffsetDateTime.now().toString()); + return response; + } + + private HardwareRuntimeStatusSection buildHardwareStatus() { + HardwareRuntimeStatusSection section = new HardwareRuntimeStatusSection(); + section.setCpu(buildCpuStatus()); + section.setMemory(buildUsageStatus(() -> hardwareStatusProbe.memoryUsage())); + section.setDisk(buildUsageStatus(() -> hardwareStatusProbe.diskUsage())); + section.setCryptoCard(buildCryptoCardStatus()); + return section; + } + + private UsageMetricStatus buildCpuStatus() { + UsageMetricStatus status = new UsageMetricStatus(); + try { + Double usage = hardwareStatusProbe.cpuUsagePercent(); + status.setUsagePercent(round(usage)); + status.setStatus(STATUS_NORMAL); + status.setMessage("正常"); + } catch (RuntimeException ex) { + status.setStatus(STATUS_UNKNOWN); + status.setMessage("未知"); + } + return status; + } + + private UsageMetricStatus buildUsageStatus(UsageSupplier supplier) { + UsageMetricStatus status = new UsageMetricStatus(); + try { + UsageSnapshot snapshot = supplier.get(); + status.setUsedGb(round(snapshot.getUsedGb())); + status.setTotalGb(round(snapshot.getTotalGb())); + status.setUsagePercent(round(snapshot.usagePercent())); + status.setStatus(STATUS_NORMAL); + status.setMessage("正常"); + } catch (RuntimeException ex) { + status.setStatus(STATUS_UNKNOWN); + status.setMessage("未知"); + } + return status; + } + + private CryptoCardRuntimeStatus buildCryptoCardStatus() { + CryptoCardRuntimeStatus status = new CryptoCardRuntimeStatus(); + try { + pcieCryptoService.getDeviceInfo(); + status.setStatus(STATUS_NORMAL); + status.setMessage("就绪状态"); + } catch (RuntimeException ex) { + status.setStatus(STATUS_ABNORMAL); + status.setMessage("异常"); + } + return status; + } + + private List buildSoftwareStatuses() { + CurrentInitConfigResponse config = loadCurrentInitConfigIfPresent(); + String productType = config == null ? normalize(cisdPresetProperties.getProductType()) : normalize(config.getProductType()); + String mqType = config == null ? "" : normalize(config.getMqType()); + + List items = new ArrayList<>(); + items.add(buildSoftwareItem("RABBITMQ", "消息队列", cached("RABBITMQ"), isRabbitmqEnabled(mqType), softwareRuntimeProbe::isRabbitmqRunning)); + items.add(buildSoftwareItem("TLQ", "消息队列", cached("TLQ"), isTlqEnabled(mqType), softwareRuntimeProbe::isTlqRunning)); + items.add(buildSoftwareItem("TIDB", "数据库", cached("TIDB"), isTidbEnabled(productType), softwareRuntimeProbe::isTidbRunning)); + items.add(buildSoftwareItem("NGINX", "Nginx", cached("NGINX"), isNginxEnabled(productType), softwareRuntimeProbe::isNginxRunning)); + items.add(buildAppItem(productType, config != null)); + items.add(buildSoftwareItem("TMS", "设备管理软件", firstNonBlank(cached("TMS"), systemSoftwareVersionProbe.tmsVersion(), cisdPresetProperties.getVersion()), true, softwareRuntimeProbe::isTmsRunning)); + return items; + } + + private SoftwareRuntimeStatusItem buildAppItem(String productType, boolean initialized) { + boolean standard = "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType); + BooleanSupplier supplier = standard ? softwareRuntimeProbe::isStandardAppRunning : softwareRuntimeProbe::isDirectAppRunning; + String name = "应用软件"; + String displayName = firstNonBlank(cached("APP"), appName(productType)); + return buildSoftwareItem("APP", name, displayName, initialized, supplier); + } + + private SoftwareRuntimeStatusItem buildSoftwareItem( + String code, + String name, + String displayName, + boolean enabled, + BooleanSupplier probe + ) { + SoftwareRuntimeStatusItem item = new SoftwareRuntimeStatusItem(); + item.setComponentCode(code); + item.setComponentName(name); + item.setDisplayName(firstNonBlank(displayName, name)); + if (!enabled) { + item.setStatus(STATUS_NOT_INSTALLED); + item.setMessage("未安装"); + return item; + } + try { + if (probe.getAsBoolean()) { + item.setStatus(STATUS_NORMAL); + item.setMessage("正常"); + } else { + item.setStatus(STATUS_ABNORMAL); + item.setMessage("异常"); + } + } catch (RuntimeException ex) { + item.setStatus(STATUS_UNKNOWN); + item.setMessage("未知"); + } + return item; + } + + private CurrentInitConfigResponse loadCurrentInitConfigIfPresent() { + try { + if (!"INITIALIZED".equals(initService.getDeviceInitStateSnapshot().initState())) { + return null; + } + return initService.loadCurrentInitConfig(); + } catch (BizException ex) { + return null; + } + } + + private boolean isRabbitmqEnabled(String mqType) { + return "RABBITMQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType) || "RABBITMQ_CFMQ".equals(mqType); + } + + private boolean isTlqEnabled(String mqType) { + return "TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType); + } + + private boolean isTidbEnabled(String productType) { + return "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType); + } + + private boolean isNginxEnabled(String productType) { + return "ENTERPRISE".equals(productType) || "INDIRECT".equals(productType); + } + + private String cached(String componentCode) { + Optional entity = deviceSoftwareVersionRepository.findByComponentCode(componentCode); + return entity.map(DeviceSoftwareVersionEntity::getCurrentVersion).orElse(""); + } + + private String appName(String productType) { + return switch (productType) { + case "ENTERPRISE" -> "标准收发器企业版"; + case "INDIRECT" -> "标准收发器间参版"; + case "DIRECT" -> "直参轻量化终端版"; + default -> "应用软件"; + }; + } + + private String normalize(String value) { + return value == null ? "" : value.trim().toUpperCase(); + } + + private double round(double value) { + return Math.round(value * 100D) / 100D; + } + + private String firstNonBlank(String... values) { + if (values == null) { + return ""; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value.trim(); + } + } + return ""; + } + + @FunctionalInterface + private interface UsageSupplier { + UsageSnapshot get(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/DefaultHardwareStatusProbe.java b/src/main/java/com/cisd/tms/modules/device/support/DefaultHardwareStatusProbe.java new file mode 100644 index 0000000..74a667a --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/DefaultHardwareStatusProbe.java @@ -0,0 +1,116 @@ +package com.cisd.tms.modules.device.support; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class DefaultHardwareStatusProbe implements HardwareStatusProbe { + + @Override + public Double cpuUsagePercent() { + CpuTimes before = readCpuTimes(); + try { + Thread.sleep(120L); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("cpu sampling interrupted", ex); + } + CpuTimes after = readCpuTimes(); + long totalDiff = after.total - before.total; + long idleDiff = after.idle - before.idle; + if (totalDiff <= 0) { + return 0D; + } + return round((1D - (idleDiff * 1D / totalDiff)) * 100D); + } + + @Override + public UsageSnapshot memoryUsage() { + long totalKb = 0L; + long availableKb = 0L; + try { + for (String line : Files.readAllLines(Path.of("/proc/meminfo"))) { + if (line.startsWith("MemTotal:")) { + totalKb = parseKb(line); + } else if (line.startsWith("MemAvailable:")) { + availableKb = parseKb(line); + } + } + } catch (IOException ex) { + throw new IllegalStateException("memory probe failed", ex); + } + if (totalKb <= 0 || availableKb < 0) { + throw new IllegalStateException("memory probe returned invalid value"); + } + double totalGb = totalKb / 1024D / 1024D; + double usedGb = (totalKb - availableKb) / 1024D / 1024D; + return new UsageSnapshot(round(usedGb), round(totalGb)); + } + + @Override + public UsageSnapshot diskUsage() { + try { + List lines = runCommand("df -k /"); + if (lines.size() < 2) { + throw new IllegalStateException("disk probe returned no data"); + } + String[] parts = lines.get(1).trim().split("\\s+"); + if (parts.length < 5) { + throw new IllegalStateException("disk probe returned invalid data"); + } + double totalGb = Long.parseLong(parts[1]) / 1024D / 1024D; + double usedGb = Long.parseLong(parts[2]) / 1024D / 1024D; + return new UsageSnapshot(round(usedGb), round(totalGb)); + } catch (IOException ex) { + throw new IllegalStateException("disk probe failed", ex); + } + } + + private CpuTimes readCpuTimes() { + try { + String line = Files.readAllLines(Path.of("/proc/stat")).get(0); + String[] parts = line.trim().split("\\s+"); + long idle = Long.parseLong(parts[4]); + long total = 0L; + for (int i = 1; i < parts.length; i++) { + total += Long.parseLong(parts[i]); + } + return new CpuTimes(idle, total); + } catch (IOException ex) { + throw new IllegalStateException("cpu probe failed", ex); + } + } + + private long parseKb(String line) { + String[] parts = line.trim().split("\\s+"); + if (parts.length < 2) { + return 0L; + } + return Long.parseLong(parts[1]); + } + + private List runCommand(String command) throws IOException { + Process process = new ProcessBuilder("bash", "-lc", command).start(); + try { + int exitCode = process.waitFor(); + List lines = new String(process.getInputStream().readAllBytes()).lines().toList(); + if (exitCode != 0) { + throw new IllegalStateException("command failed: " + command); + } + return lines; + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("command interrupted: " + command, ex); + } + } + + private double round(double value) { + return Math.round(value * 100D) / 100D; + } + + private record CpuTimes(long idle, long total) { + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/DefaultSoftwareRuntimeProbe.java b/src/main/java/com/cisd/tms/modules/device/support/DefaultSoftwareRuntimeProbe.java new file mode 100644 index 0000000..1d5c47d --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/DefaultSoftwareRuntimeProbe.java @@ -0,0 +1,62 @@ +package com.cisd.tms.modules.device.support; + +import org.springframework.stereotype.Component; + +@Component +public class DefaultSoftwareRuntimeProbe implements SoftwareRuntimeProbe { + + @Override + public boolean isRabbitmqRunning() { + return isRunning("rabbitmq-server"); + } + + @Override + public boolean isTlqRunning() { + return isRunning("tonglink|TLQ"); + } + + @Override + public boolean isTidbRunning() { + return isRunning("tidb-server"); + } + + @Override + public boolean isNginxRunning() { + return isRunning("nginx"); + } + + @Override + public boolean isStandardAppRunning() { + return isRunning("cmsp") && isRunning("cmtp"); + } + + @Override + public boolean isDirectAppRunning() { + return isRunning("cmep4d|CAE"); + } + + @Override + public boolean isTmsRunning() { + return isRunning("tms-framework.jar"); + } + + private boolean isRunning(String pattern) { + Process process; + try { + process = new ProcessBuilder("bash", "-lc", "pgrep -f '" + pattern + "' >/dev/null").start(); + int exitCode = process.waitFor(); + if (exitCode == 0) { + return true; + } + if (exitCode == 1) { + return false; + } + throw new IllegalStateException("pgrep failed for pattern: " + pattern); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("process probe interrupted: " + pattern, ex); + } catch (Exception ex) { + throw new IllegalStateException("process probe failed: " + pattern, ex); + } + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/DefaultSystemHardwareProbe.java b/src/main/java/com/cisd/tms/modules/device/support/DefaultSystemHardwareProbe.java new file mode 100644 index 0000000..5298461 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/DefaultSystemHardwareProbe.java @@ -0,0 +1,60 @@ +package com.cisd.tms.modules.device.support; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import org.springframework.stereotype.Component; + +@Component +public class DefaultSystemHardwareProbe implements SystemHardwareProbe { + + @Override + public String cpuModel() { + return firstLine("lscpu | awk -F: '/Model name/ {print $2}'"); + } + + @Override + public String memoryTotal() { + try { + for (String line : Files.readAllLines(Path.of("/proc/meminfo"))) { + if (!line.startsWith("MemTotal:")) { + continue; + } + String[] parts = line.split("\\s+"); + if (parts.length < 2) { + return ""; + } + long kb = Long.parseLong(parts[1]); + double gb = kb / 1024.0 / 1024.0; + return String.format(Locale.ROOT, "%.0fGB", gb); + } + } catch (IOException | NumberFormatException ignored) { + return ""; + } + return ""; + } + + @Override + public String diskTotal() { + return firstLine("df -h / | awk 'NR==2 {print $2}'"); + } + + private String firstLine(String command) { + try { + Process process = new ProcessBuilder("bash", "-lc", command).start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line = reader.readLine(); + process.waitFor(); + return line == null ? "" : line.trim(); + } + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + return ""; + } catch (IOException ignored) { + return ""; + } + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/DefaultSystemSoftwareVersionProbe.java b/src/main/java/com/cisd/tms/modules/device/support/DefaultSystemSoftwareVersionProbe.java new file mode 100644 index 0000000..09284ec --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/DefaultSystemSoftwareVersionProbe.java @@ -0,0 +1,61 @@ +package com.cisd.tms.modules.device.support; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import org.springframework.stereotype.Component; + +@Component +public class DefaultSystemSoftwareVersionProbe implements SystemSoftwareVersionProbe { + + @Override + public String osVersion() { + try { + for (String line : Files.readAllLines(Path.of("/etc/os-release"))) { + if (!line.startsWith("PRETTY_NAME=")) { + continue; + } + return line.substring("PRETTY_NAME=".length()).replace("\"", "").trim(); + } + } catch (IOException ignored) { + return ""; + } + return ""; + } + + @Override + public String tmsVersion() { + Package pkg = DefaultSystemSoftwareVersionProbe.class.getPackage(); + return pkg == null || pkg.getImplementationVersion() == null ? "" : pkg.getImplementationVersion().trim(); + } + + @Override + public String rabbitmqVersion() { + return ""; + } + + @Override + public String tlqVersion() { + return ""; + } + + @Override + public String tidbVersion() { + return ""; + } + + @Override + public String nginxVersion() { + return ""; + } + + @Override + public String standardAppVersion(String productType) { + return ""; + } + + @Override + public String directAppVersion() { + return ""; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/HardwareStatusProbe.java b/src/main/java/com/cisd/tms/modules/device/support/HardwareStatusProbe.java new file mode 100644 index 0000000..32ff79b --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/HardwareStatusProbe.java @@ -0,0 +1,10 @@ +package com.cisd.tms.modules.device.support; + +public interface HardwareStatusProbe { + + Double cpuUsagePercent(); + + UsageSnapshot memoryUsage(); + + UsageSnapshot diskUsage(); +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/SoftwareRuntimeProbe.java b/src/main/java/com/cisd/tms/modules/device/support/SoftwareRuntimeProbe.java new file mode 100644 index 0000000..05d49d2 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/SoftwareRuntimeProbe.java @@ -0,0 +1,18 @@ +package com.cisd.tms.modules.device.support; + +public interface SoftwareRuntimeProbe { + + boolean isRabbitmqRunning(); + + boolean isTlqRunning(); + + boolean isTidbRunning(); + + boolean isNginxRunning(); + + boolean isStandardAppRunning(); + + boolean isDirectAppRunning(); + + boolean isTmsRunning(); +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/SystemHardwareProbe.java b/src/main/java/com/cisd/tms/modules/device/support/SystemHardwareProbe.java new file mode 100644 index 0000000..5297679 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/SystemHardwareProbe.java @@ -0,0 +1,10 @@ +package com.cisd.tms.modules.device.support; + +public interface SystemHardwareProbe { + + String cpuModel(); + + String memoryTotal(); + + String diskTotal(); +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/SystemSoftwareVersionProbe.java b/src/main/java/com/cisd/tms/modules/device/support/SystemSoftwareVersionProbe.java new file mode 100644 index 0000000..8347055 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/SystemSoftwareVersionProbe.java @@ -0,0 +1,20 @@ +package com.cisd.tms.modules.device.support; + +public interface SystemSoftwareVersionProbe { + + String osVersion(); + + String tmsVersion(); + + String rabbitmqVersion(); + + String tlqVersion(); + + String tidbVersion(); + + String nginxVersion(); + + String standardAppVersion(String productType); + + String directAppVersion(); +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/UsageSnapshot.java b/src/main/java/com/cisd/tms/modules/device/support/UsageSnapshot.java new file mode 100644 index 0000000..af3e1ab --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/UsageSnapshot.java @@ -0,0 +1,27 @@ +package com.cisd.tms.modules.device.support; + +public class UsageSnapshot { + + private final double usedGb; + private final double totalGb; + + public UsageSnapshot(double usedGb, double totalGb) { + this.usedGb = usedGb; + this.totalGb = totalGb; + } + + public double getUsedGb() { + return usedGb; + } + + public double getTotalGb() { + return totalGb; + } + + public double usagePercent() { + if (totalGb <= 0) { + return 0D; + } + return usedGb * 100D / totalGb; + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/crypto/CryptoPayloadCodec.java b/src/main/java/com/cisd/tms/modules/device/support/crypto/CryptoPayloadCodec.java new file mode 100644 index 0000000..0939066 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/crypto/CryptoPayloadCodec.java @@ -0,0 +1,106 @@ +package com.cisd.tms.modules.device.support.crypto; + +import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoRecoverLmkExInternalRequest; +import java.util.Arrays; +import java.util.HexFormat; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class CryptoPayloadCodec { + + private CryptoPayloadCodec() { + } + + public static byte[] decodeNullableBase64(String value) { + if (value == null || value.isBlank()) { + return null; + } + byte[] decoded = java.util.Base64.getDecoder().decode(value); + return decoded.length == 0 ? null : Arrays.copyOf(decoded, decoded.length); + } + + public static byte[] decodeBase64(String value) { + byte[] decoded = java.util.Base64.getDecoder().decode(value); + return Arrays.copyOf(decoded, decoded.length); + } + + public static Map hexPayload(String hexKey, byte[] data, int length) { + byte[] normalized = normalizeBinary(data, length); + return Map.of( + "length", normalized.length, + hexKey, HexFormat.of().formatHex(normalized) + ); + } + + public static Map lmkComponentPayload(BackupDataResult result) { + byte[] normalized = normalizeBinary(result.getData(), result.getLength()); + if (normalized.length < 56) { + throw new IllegalStateException("Invalid LMK component length: " + normalized.length); + } + + HexFormat hexFormat = HexFormat.of(); + Map components = new LinkedHashMap<>(); + for (int index = 0; index < 3; index++) { + int start = 8 + (index * 16); + components.put(String.valueOf(index + 1), hexFormat.formatHex(Arrays.copyOfRange(normalized, start, start + 16))); + } + + Map payload = new LinkedHashMap<>(); + payload.put("length", normalized.length); + payload.put("lmkMacHex", hexFormat.formatHex(Arrays.copyOf(normalized, 8))); + payload.put("components", components); + return payload; + } + + public static byte[] toLmkComponentBytes(CryptoRecoverLmkExInternalRequest request) { + if (request.getComponentBase64() != null && !request.getComponentBase64().isBlank()) { + return decodeBase64(request.getComponentBase64()); + } + + String lmkMacHex = requireText("lmkMacHex", request.getLmkMacHex()); + Map components = request.getComponents(); + if (components == null || components.isEmpty()) { + throw new IllegalArgumentException("components must not be empty"); + } + + byte[] result = new byte[56]; + copyHexTo(result, 0, lmkMacHex, 8, "lmkMacHex"); + copyHexTo(result, 8, requireComponentHex(components, 1), 16, "components[1]"); + copyHexTo(result, 24, requireComponentHex(components, 2), 16, "components[2]"); + copyHexTo(result, 40, requireComponentHex(components, 3), 16, "components[3]"); + return result; + } + + private static String requireComponentHex(Map components, int index) { + return requireText("components[" + index + "]", components.get(String.valueOf(index))); + } + + private static String requireText(String fieldName, String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(fieldName + " must not be blank"); + } + return value; + } + + private static void copyHexTo(byte[] target, int offset, String hex, int expectedBytes, String fieldName) { + byte[] decoded; + try { + decoded = HexFormat.of().parseHex(hex); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException(fieldName + " must be valid HEX", e); + } + if (decoded.length != expectedBytes) { + throw new IllegalArgumentException(fieldName + " length must be " + expectedBytes + " bytes"); + } + System.arraycopy(decoded, 0, target, offset, decoded.length); + } + + private static byte[] normalizeBinary(byte[] data, int length) { + if (data == null || data.length == 0) { + return new byte[0]; + } + int safeLength = length > 0 && length <= data.length ? length : data.length; + return Arrays.copyOf(data, safeLength); + } +} diff --git a/src/main/java/com/cisd/tms/modules/device/support/crypto/CryptoRequestMapper.java b/src/main/java/com/cisd/tms/modules/device/support/crypto/CryptoRequestMapper.java new file mode 100644 index 0000000..65a3bc0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/device/support/crypto/CryptoRequestMapper.java @@ -0,0 +1,179 @@ +package com.cisd.tms.modules.device.support.crypto; + +import com.cisd.tms.integration.crypto.pcie.model.ChangePrivateKeyPasswordRequest; +import com.cisd.tms.integration.crypto.pcie.model.GenerateKeyPairRequest; +import com.cisd.tms.integration.crypto.pcie.model.ImportEncKeyPairEccRequest; +import com.cisd.tms.integration.crypto.pcie.model.ImportEncKeyPairRsaRequest; +import com.cisd.tms.integration.crypto.pcie.model.ImportKekRequest; +import com.cisd.tms.integration.crypto.pcie.model.ImportKeyPairRequest; +import com.cisd.tms.integration.crypto.pcie.model.KekComponentRequest; +import com.cisd.tms.integration.crypto.pcie.model.PrivateKeyAccessRequest; +import com.cisd.tms.integration.crypto.pcie.model.RecoverKekRequest; +import com.cisd.tms.integration.crypto.pcie.model.RecoverUserKeyRequest; +import com.cisd.tms.integration.crypto.pcie.model.SymmetricCryptoRequest; +import com.cisd.tms.integration.crypto.pcie.model.SymmetricKekCryptoRequest; +import com.cisd.tms.integration.crypto.pcie.model.UkeyGenerateRequest; +import com.cisd.tms.integration.crypto.pcie.model.UkeyLoginRequest; +import com.cisd.tms.integration.crypto.pcie.model.UkeyPinChangeRequest; +import com.cisd.tms.integration.crypto.pcie.model.UkeyUnlockRequest; +import com.cisd.tms.integration.crypto.pcie.model.UpdateBackedKeyRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoChangePrivateKeyPasswordInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoGenerateKeyPairInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoImportEncryptedKeyPairEccInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoImportEncryptedKeyPairRsaInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoImportKekInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoImportKeyPairInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoKekComponentImportInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoPrivateKeyAccessInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoRecoverKekInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoRecoverUserKeyInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoSymmetricInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoSymmetricKekInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoUkeyGenerateInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoUkeyLoginInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoUkeyPinChangeInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoUkeyUnlockInternalRequest; +import com.cisd.tms.modules.device.dto.crypto.request.CryptoUpdateBackedKeyInternalRequest; + +public final class CryptoRequestMapper { + + private CryptoRequestMapper() { + } + + public static SymmetricCryptoRequest toSymmetricCryptoRequest(CryptoSymmetricInternalRequest request) { + SymmetricCryptoRequest cryptoRequest = new SymmetricCryptoRequest(); + cryptoRequest.setAlgId(request.getAlgId()); + cryptoRequest.setKey(CryptoPayloadCodec.decodeBase64(request.getKeyBase64())); + cryptoRequest.setIv(CryptoPayloadCodec.decodeNullableBase64(request.getIvBase64())); + cryptoRequest.setData(CryptoPayloadCodec.decodeBase64(request.getDataBase64())); + cryptoRequest.setOutBufferSize(request.getOutBufferSize()); + return cryptoRequest; + } + + public static SymmetricKekCryptoRequest toSymmetricKekCryptoRequest(CryptoSymmetricKekInternalRequest request) { + SymmetricKekCryptoRequest cryptoRequest = new SymmetricKekCryptoRequest(); + cryptoRequest.setAlgId(request.getAlgId()); + cryptoRequest.setKekIndex(request.getKekIndex()); + cryptoRequest.setWrappedKey(CryptoPayloadCodec.decodeBase64(request.getWrappedKeyBase64())); + cryptoRequest.setIv(CryptoPayloadCodec.decodeNullableBase64(request.getIvBase64())); + cryptoRequest.setData(CryptoPayloadCodec.decodeBase64(request.getDataBase64())); + cryptoRequest.setOutBufferSize(request.getOutBufferSize()); + return cryptoRequest; + } + + public static GenerateKeyPairRequest toGenerateKeyPairRequest(CryptoGenerateKeyPairInternalRequest request) { + GenerateKeyPairRequest req = new GenerateKeyPairRequest(); + req.setKeyBits(request.getKeyBits()); + req.setKeyIndex(request.getKeyIndex()); + req.setKeyType(request.getKeyType()); + return req; + } + + public static ImportKeyPairRequest toImportKeyPairRequest(CryptoImportKeyPairInternalRequest request) { + ImportKeyPairRequest req = new ImportKeyPairRequest(); + req.setKeyIndex(request.getKeyIndex()); + req.setKeyType(request.getKeyType()); + req.setPublicKey(CryptoPayloadCodec.decodeBase64(request.getPublicKeyBase64())); + req.setPrivateKey(CryptoPayloadCodec.decodeBase64(request.getPrivateKeyBase64())); + return req; + } + + public static ChangePrivateKeyPasswordRequest toChangePrivateKeyPasswordRequest(CryptoChangePrivateKeyPasswordInternalRequest request) { + ChangePrivateKeyPasswordRequest req = new ChangePrivateKeyPasswordRequest(); + req.setKeyIndex(request.getKeyIndex()); + req.setOldPassword(CryptoPayloadCodec.decodeBase64(request.getOldPasswordBase64())); + req.setNewPassword(CryptoPayloadCodec.decodeBase64(request.getNewPasswordBase64())); + return req; + } + + public static PrivateKeyAccessRequest toPrivateKeyAccessRequest(CryptoPrivateKeyAccessInternalRequest request) { + PrivateKeyAccessRequest req = new PrivateKeyAccessRequest(); + req.setKeyIndex(request.getKeyIndex()); + req.setPassword(CryptoPayloadCodec.decodeBase64(request.getPasswordBase64())); + return req; + } + + public static ImportKekRequest toImportKekRequest(CryptoImportKekInternalRequest request) { + ImportKekRequest req = new ImportKekRequest(); + req.setKeyIndex(request.getKeyIndex()); + req.setKeyData(CryptoPayloadCodec.decodeBase64(request.getKeyDataBase64())); + return req; + } + + public static KekComponentRequest toKekComponentRequest(CryptoKekComponentImportInternalRequest request) { + KekComponentRequest req = new KekComponentRequest(); + req.setKeyIndex(request.getKeyIndex()); + req.setComponentNum(request.getComponentNum()); + req.setComponentData(CryptoPayloadCodec.decodeBase64(request.getComponentDataBase64())); + return req; + } + + public static RecoverUserKeyRequest toRecoverUserKeyRequest(CryptoRecoverUserKeyInternalRequest request) { + RecoverUserKeyRequest req = new RecoverUserKeyRequest(); + req.setKeyIndex(request.getKeyIndex()); + req.setKeyType(request.getKeyType()); + req.setEncryptedKey(CryptoPayloadCodec.decodeBase64(request.getEncryptedKeyBase64())); + req.setStoreFlag(request.getStoreFlag()); + return req; + } + + public static RecoverKekRequest toRecoverKekRequest(CryptoRecoverKekInternalRequest request) { + RecoverKekRequest req = new RecoverKekRequest(); + req.setKeyIndex(request.getKeyIndex()); + req.setEncryptedKey(CryptoPayloadCodec.decodeBase64(request.getEncryptedKeyBase64())); + return req; + } + + public static ImportEncKeyPairEccRequest toImportEncryptedKeyPairEccRequest(CryptoImportEncryptedKeyPairEccInternalRequest request) { + ImportEncKeyPairEccRequest req = new ImportEncKeyPairEccRequest(); + req.setKeyIndex(request.getKeyIndex()); + req.setEnvelopedKeyBlob(CryptoPayloadCodec.decodeBase64(request.getEnvelopedKeyBlobBase64())); + return req; + } + + public static ImportEncKeyPairRsaRequest toImportEncryptedKeyPairRsaRequest(CryptoImportEncryptedKeyPairRsaInternalRequest request) { + ImportEncKeyPairRsaRequest req = new ImportEncKeyPairRsaRequest(); + req.setKeyIndex(request.getKeyIndex()); + req.setSymAlgId(request.getSymAlgId()); + req.setWrappedKey(CryptoPayloadCodec.decodeBase64(request.getWrappedKeyBase64())); + req.setEncryptedData(CryptoPayloadCodec.decodeBase64(request.getEncryptedDataBase64())); + return req; + } + + public static UpdateBackedKeyRequest toUpdateBackedKeyRequest(CryptoUpdateBackedKeyInternalRequest request) { + UpdateBackedKeyRequest req = new UpdateBackedKeyRequest(); + req.setOldData(CryptoPayloadCodec.decodeBase64(request.getOldDataBase64())); + req.setNewDataBufferSize(request.getNewDataBufferSize()); + return req; + } + + public static UkeyGenerateRequest toUkeyGenerateRequest(CryptoUkeyGenerateInternalRequest request) { + UkeyGenerateRequest req = new UkeyGenerateRequest(); + req.setAdminIndex(request.getAdminIndex()); + req.setUkeyPin(CryptoPayloadCodec.decodeBase64(request.getUkeyPinBase64())); + req.setLoginPin(CryptoPayloadCodec.decodeBase64(request.getLoginPinBase64())); + return req; + } + + public static UkeyLoginRequest toUkeyLoginRequest(CryptoUkeyLoginInternalRequest request) { + UkeyLoginRequest req = new UkeyLoginRequest(); + req.setUkeyPin(CryptoPayloadCodec.decodeBase64(request.getUkeyPinBase64())); + req.setLoginPin(CryptoPayloadCodec.decodeBase64(request.getLoginPinBase64())); + return req; + } + + public static UkeyPinChangeRequest toUkeyPinChangeRequest(CryptoUkeyPinChangeInternalRequest request) { + UkeyPinChangeRequest req = new UkeyPinChangeRequest(); + req.setPinType(request.getPinType()); + req.setOldPin(CryptoPayloadCodec.decodeBase64(request.getOldPinBase64())); + req.setNewPin(CryptoPayloadCodec.decodeBase64(request.getNewPinBase64())); + return req; + } + + public static UkeyUnlockRequest toUkeyUnlockRequest(CryptoUkeyUnlockInternalRequest request) { + UkeyUnlockRequest req = new UkeyUnlockRequest(); + req.setAdminPin(CryptoPayloadCodec.decodeBase64(request.getAdminPinBase64())); + req.setUserPin(CryptoPayloadCodec.decodeBase64(request.getUserPinBase64())); + return req; + } +} diff --git a/src/main/java/com/cisd/tms/modules/init/dto/CurrentInitConfigResponse.java b/src/main/java/com/cisd/tms/modules/init/dto/CurrentInitConfigResponse.java new file mode 100644 index 0000000..052547e --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/init/dto/CurrentInitConfigResponse.java @@ -0,0 +1,138 @@ +package com.cisd.tms.modules.init.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "当前已初始化的 CISD 核心配置响应") +public class CurrentInitConfigResponse { + + @Schema(description = "机构号类型", example = "BIC") + private String orgCodeType; + @Schema(description = "机构号", example = "AAAABBBBXXX") + private String orgCode; + @Schema(description = "机构中文名", example = "测试机构") + private String orgNameCn; + @Schema(description = "机构英文名", example = "Test Organization") + private String orgNameEn; + @Schema(description = "产品类型", example = "ENTERPRISE") + private String productType; + @Schema(description = "部署模式", example = "SINGLE") + private String deployMode; + @Schema(description = "中间件类型", example = "RABBITMQ") + private String mqType; + @Schema(description = "业务通道用户名", example = "cisd_channel_user") + private String channelUsername; + @Schema(description = "节点一 IP", example = "10.0.0.1") + private String node01Ip; + @Schema(description = "节点二 IP", example = "10.0.0.2") + private String node02Ip; + @Schema(description = "签名服务地址", example = "172.30.1.101") + private String signHost; + @Schema(description = "签名服务端口", example = "50006") + private Integer signPort; + @Schema(description = "签名服务类型", example = "INFSEC") + private String signType; + + public String getOrgCodeType() { + return orgCodeType; + } + + public void setOrgCodeType(String orgCodeType) { + this.orgCodeType = orgCodeType; + } + + public String getOrgCode() { + return orgCode; + } + + public void setOrgCode(String orgCode) { + this.orgCode = orgCode; + } + + public String getOrgNameCn() { + return orgNameCn; + } + + public void setOrgNameCn(String orgNameCn) { + this.orgNameCn = orgNameCn; + } + + public String getOrgNameEn() { + return orgNameEn; + } + + public void setOrgNameEn(String orgNameEn) { + this.orgNameEn = orgNameEn; + } + + public String getProductType() { + return productType; + } + + public void setProductType(String productType) { + this.productType = productType; + } + + public String getDeployMode() { + return deployMode; + } + + public void setDeployMode(String deployMode) { + this.deployMode = deployMode; + } + + public String getMqType() { + return mqType; + } + + public void setMqType(String mqType) { + this.mqType = mqType; + } + + public String getChannelUsername() { + return channelUsername; + } + + public void setChannelUsername(String channelUsername) { + this.channelUsername = channelUsername; + } + + public String getNode01Ip() { + return node01Ip; + } + + public void setNode01Ip(String node01Ip) { + this.node01Ip = node01Ip; + } + + public String getNode02Ip() { + return node02Ip; + } + + public void setNode02Ip(String node02Ip) { + this.node02Ip = node02Ip; + } + + public String getSignHost() { + return signHost; + } + + public void setSignHost(String signHost) { + this.signHost = signHost; + } + + public Integer getSignPort() { + return signPort; + } + + public void setSignPort(Integer signPort) { + this.signPort = signPort; + } + + public String getSignType() { + return signType; + } + + public void setSignType(String signType) { + this.signType = signType; + } +} diff --git a/src/main/java/com/cisd/tms/modules/init/dto/ResetCreateTaskResponse.java b/src/main/java/com/cisd/tms/modules/init/dto/ResetCreateTaskResponse.java new file mode 100644 index 0000000..a3f88ae --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/init/dto/ResetCreateTaskResponse.java @@ -0,0 +1,62 @@ +package com.cisd.tms.modules.init.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "重置任务创建响应") +public class ResetCreateTaskResponse { + + @Schema(description = "重置任务唯一标识", example = "RESET-20260312-000001") + private String taskId; + + @Schema(description = "任务状态", example = "PENDING") + private String status; + + @Schema(description = "任务类型", example = "RESET") + private String taskType; + + @Schema(description = "解析后的产品类型", example = "ENTERPRISE") + private String resolvedProductType; + + @Schema(description = "命中的预制版本", example = "V1.3.2") + private String presetVersion; + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getTaskType() { + return taskType; + } + + public void setTaskType(String taskType) { + this.taskType = taskType; + } + + public String getResolvedProductType() { + return resolvedProductType; + } + + public void setResolvedProductType(String resolvedProductType) { + this.resolvedProductType = resolvedProductType; + } + + public String getPresetVersion() { + return presetVersion; + } + + public void setPresetVersion(String presetVersion) { + this.presetVersion = presetVersion; + } +} diff --git a/src/main/java/com/cisd/tms/modules/init/dto/ResetPreviewResponse.java b/src/main/java/com/cisd/tms/modules/init/dto/ResetPreviewResponse.java new file mode 100644 index 0000000..f0ad868 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/init/dto/ResetPreviewResponse.java @@ -0,0 +1,51 @@ +package com.cisd.tms.modules.init.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "重置预检响应") +public class ResetPreviewResponse { + + @Schema(description = "解析后的产品类型", example = "ENTERPRISE") + private String resolvedProductType; + + @Schema(description = "命中的预制版本", example = "V1.3.2") + private String presetVersion; + + @Schema(description = "预估步骤数", example = "8") + private int stepCount; + + @Schema(description = "预检摘要", example = "reset preview generated") + private String summary; + + public String getResolvedProductType() { + return resolvedProductType; + } + + public void setResolvedProductType(String resolvedProductType) { + this.resolvedProductType = resolvedProductType; + } + + public String getPresetVersion() { + return presetVersion; + } + + public void setPresetVersion(String presetVersion) { + this.presetVersion = presetVersion; + } + + public int getStepCount() { + return stepCount; + } + + public void setStepCount(int stepCount) { + this.stepCount = stepCount; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } +} diff --git a/src/main/java/com/cisd/tms/modules/mk/config/MasterKeyInitProperties.java b/src/main/java/com/cisd/tms/modules/mk/config/MasterKeyInitProperties.java new file mode 100644 index 0000000..2ff3e30 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/mk/config/MasterKeyInitProperties.java @@ -0,0 +1,26 @@ +package com.cisd.tms.modules.mk.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "tms.mk.init-identify") +public class MasterKeyInitProperties { + + private String oldPinBase64 = ""; + private String newPinBase64 = ""; + + public String getOldPinBase64() { + return oldPinBase64; + } + + public void setOldPinBase64(String oldPinBase64) { + this.oldPinBase64 = oldPinBase64; + } + + public String getNewPinBase64() { + return newPinBase64; + } + + public void setNewPinBase64(String newPinBase64) { + this.newPinBase64 = newPinBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/mk/controller/LmkController.java b/src/main/java/com/cisd/tms/modules/mk/controller/LmkController.java index f489868..f8613a0 100644 --- a/src/main/java/com/cisd/tms/modules/mk/controller/LmkController.java +++ b/src/main/java/com/cisd/tms/modules/mk/controller/LmkController.java @@ -1,9 +1,9 @@ package com.cisd.tms.modules.mk.controller; import com.cisd.tms.common.api.ApiResponse; -import com.cisd.tms.modules.mk.dto.LMK; -import com.cisd.tms.modules.mk.dto.MasterKeyRestoreDTO; -import com.cisd.tms.modules.mk.dto.RecoveryResult; +import com.cisd.tms.modules.mk.dto.MasterKeyBackupMaterial; +import com.cisd.tms.modules.mk.dto.MasterKeyRecoverRequest; +import com.cisd.tms.modules.mk.dto.MasterKeyStateResult; import com.cisd.tms.modules.mk.dto.UKeySignDTO; import com.cisd.tms.modules.mk.dto.UKeySignEntity; import com.cisd.tms.modules.mk.dto.UKeySignResult; @@ -13,19 +13,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; +import jakarta.validation.Valid; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.io.IOException; -import java.io.StreamCorruptedException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - /** * @author liulu * @since 2026/3/4 @@ -35,30 +29,59 @@ import java.util.List; @Tag(name = "主密钥管理", description = "LMK 生成、恢复、状态查询和销毁接口") public class LmkController { - @Resource - private LmkService lmkService; - @Resource - private ObjectMapper objectMapper; + private final LmkService lmkService; + private final ObjectMapper objectMapper; - /** - * 生成主密钥 - * @return - */ - @PostMapping("/masterkey/generate") - @Operation(summary = "生成主密钥", description = "生成新的主密钥并返回密钥信息。") - public ApiResponse generateMasterKey() { - LMK lmk = lmkService.generateLMK(); - return ApiResponse.success(lmk); + public LmkController(LmkService lmkService, ObjectMapper objectMapper) { + this.lmkService = lmkService; + this.objectMapper = objectMapper; + } + + @PostMapping("/masterKey/init") + @Operation(summary = "初始化主密钥", description = "执行密码卡初始化识别、主密钥生成加载、内部密钥生成,并返回主密钥状态与校验值。") + public ApiResponse initMasterKey() { + return ApiResponse.success(lmkService.initMasterKey()); + } + + @PostMapping("/masterKey/destroy") + @Operation(summary = "销毁主密钥", description = "销毁当前密码卡中的主密钥,失败直接抛出异常。") + public ApiResponse destroyMasterKey() { + lmkService.destroyMasterKey(); + return ApiResponse.success(); + } + + @GetMapping("/masterKey/status") + @Operation(summary = "查询主密钥状态", description = "返回当前主密钥是否存在;存在时返回校验值。") + public ApiResponse queryMasterKeyStatus() { + return ApiResponse.success(lmkService.queryMasterKeyStatus()); + } + + @PostMapping("/masterKey/backup") + @Operation(summary = "备份主密钥分量至UKey", description = "根据请求中的 UKey 身份信息生成签名,并按照 uid 作为分量索引返回对应的主密钥分量,同时返回恢复 LMK 必需的 lmkMac。uid 取值为 1、2、3。") + public ApiResponse uKeySign(@Valid @RequestBody UKeySignDTO uKeySignDTO) { + String signValue = signUkeyPayload(uKeySignDTO); + int componentIndex = Integer.parseInt(uKeySignDTO.getUid()); + MasterKeyBackupMaterial material = lmkService.getBackupMaterial(componentIndex); + return ApiResponse.success(UKeySignResult.builder() + .sign(signValue) + .component(material.getComponent()) + .lmkMac(material.getLmkMac()) + .build()); } /** - * UKey签名并获取主密钥组件 - * @param uKeySignDTO - * @return + * 恢复主密钥。 + * 请求体需同时传入 lmkMac 和 3 个按顺序排列的主密钥分量字符串。 + * 第 1 个元素对应分量 1,第 2 个元素对应分量 2,第 3 个元素对应分量 3。 */ - @PostMapping("/masterkey/uKeySignAndKeyComponent") - @Operation(summary = "UKey 签名并获取密钥分量", description = "根据 UKey 签名结果导出内部密钥公钥并返回主密钥分量。") - public ApiResponse signAndKeyComponent(@RequestBody UKeySignDTO uKeySignDTO) { + @PostMapping("/masterKey/recover") + @Operation(summary = "恢复 LMK", description = "请求体需传入 8 字节校验值 lmkMac 和 3 个按顺序排列的主密钥分量字符串,列表下标 0、1、2 分别对应分量 1、2、3。服务端按 SDFE_LmkComponent 结构拼接后执行恢复并加载 LMK。") + public ApiResponse recoverLmkEx(@Valid @RequestBody MasterKeyRecoverRequest request) { + lmkService.recoverLmkEx(request.getLmkMac(), request.getComponents()); + return ApiResponse.success(); + } + + private String signUkeyPayload(UKeySignDTO uKeySignDTO) { String iPubKey = lmkService.exportIkPublicKeyHex(); String payload; try { @@ -66,62 +89,7 @@ public class LmkController { } catch (JsonProcessingException e) { throw new IllegalStateException("serialize sign payload failed", e); } - String signValue = lmkService.signIk(payload); - int uid = Integer.parseInt(uKeySignDTO.getUid()); - String components = lmkService.getComponent(uid); - return ApiResponse.success(UKeySignResult.builder().sign(signValue).extra(components).build()); - } - - @GetMapping("/masterKeyStatus") - @Operation(summary = "查询主密钥状态", description = "返回主密钥当前是否存在以及相关状态详情。") - public ApiResponse getMasterKeyStatus() { - return ApiResponse.success(lmkService.getMasterKeyStatus()); - } - - /** - * 恢复主密钥 - * @param masterKeyRestoreDTO - * @return - * @throws IOException - * @throws ClassNotFoundException - */ - @PostMapping("/recoveryMasterKey") - @Operation(summary = "恢复主密钥", description = "根据上传的分量或序列化载荷恢复主密钥,并在需要时重建内部密钥。") - public ApiResponse recoveryMasterKey(@RequestBody MasterKeyRestoreDTO masterKeyRestoreDTO) - throws IOException, ClassNotFoundException { - RecoveryResult result; - String mac; - try { - Collection keyComponents = masterKeyRestoreDTO.getComponents().values(); - List keyComponentsList = new ArrayList<>(keyComponents); - mac = lmkService.recoveryLMK(keyComponentsList); - result = RecoveryResult.getInstance(false, mac); - } catch (StreamCorruptedException streamCorruptedException) { - String masterKey = lmkService.masterKeyCompose(masterKeyRestoreDTO.getComponents()); - mac = lmkService.recoveryLMKAndGenIK(masterKey); - result = RecoveryResult.getInstance(true, mac); - } - return ApiResponse.success(result); - } - - /** - * 分量合成恢复主密钥 - * @param masterKeyRestoreDTO - * @return - */ - @PostMapping("/masterKeyRecovery") - @Operation(summary = "分量合成恢复主密钥", description = "根据主密钥分量直接合成并恢复主密钥。") - public ApiResponse masterKeyRecovery(@RequestBody MasterKeyRestoreDTO masterKeyRestoreDTO) { - String masterKey = lmkService.masterKeyCompose(masterKeyRestoreDTO.getComponents()); - LMK lmk = lmkService.recoveryLMK1(LMK.getInstance(masterKey, masterKeyRestoreDTO)); - return ApiResponse.success(lmk); - } - - @GetMapping("/destroyLMKAndIK") - @Operation(summary = "销毁主密钥和内部密钥", description = "销毁当前主密钥以及相关内部密钥材料。") - public ApiResponse destroyLMKAndIK() { - lmkService.destroyLMKAndIK(); - return ApiResponse.success("主密钥和内部密钥销毁成功!"); + return lmkService.signIk(payload); } } diff --git a/src/main/java/com/cisd/tms/modules/mk/dto/MasterKeyBackupMaterial.java b/src/main/java/com/cisd/tms/modules/mk/dto/MasterKeyBackupMaterial.java new file mode 100644 index 0000000..93cc308 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/mk/dto/MasterKeyBackupMaterial.java @@ -0,0 +1,20 @@ +package com.cisd.tms.modules.mk.dto; + +public class MasterKeyBackupMaterial { + + private final String component; + private final String lmkMac; + + public MasterKeyBackupMaterial(String component, String lmkMac) { + this.component = component; + this.lmkMac = lmkMac; + } + + public String getComponent() { + return component; + } + + public String getLmkMac() { + return lmkMac; + } +} diff --git a/src/main/java/com/cisd/tms/modules/mk/dto/MasterKeyRecoverRequest.java b/src/main/java/com/cisd/tms/modules/mk/dto/MasterKeyRecoverRequest.java new file mode 100644 index 0000000..1fe5e9c --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/mk/dto/MasterKeyRecoverRequest.java @@ -0,0 +1,33 @@ +package com.cisd.tms.modules.mk.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; + +import java.util.List; + +public class MasterKeyRecoverRequest { + + @NotBlank(message = "lmkMac must not be blank") + private String lmkMac; + + @NotEmpty(message = "components must not be empty") + @Size(min = 3, max = 3, message = "components size must be 3") + private List<@NotBlank(message = "component must not be blank") String> components; + + public String getLmkMac() { + return lmkMac; + } + + public void setLmkMac(String lmkMac) { + this.lmkMac = lmkMac; + } + + public List getComponents() { + return components; + } + + public void setComponents(List components) { + this.components = components; + } +} diff --git a/src/main/java/com/cisd/tms/modules/mk/dto/MasterKeyStateResult.java b/src/main/java/com/cisd/tms/modules/mk/dto/MasterKeyStateResult.java new file mode 100644 index 0000000..fb9ffe0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/mk/dto/MasterKeyStateResult.java @@ -0,0 +1,23 @@ +package com.cisd.tms.modules.mk.dto; + +public class MasterKeyStateResult { + + private boolean status; + private String seedMac; + + public boolean isStatus() { + return status; + } + + public void setStatus(boolean status) { + this.status = status; + } + + public String getSeedMac() { + return seedMac; + } + + public void setSeedMac(String seedMac) { + this.seedMac = seedMac; + } +} diff --git a/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignResult.java b/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignResult.java index 19a15e8..b49fa47 100644 --- a/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignResult.java +++ b/src/main/java/com/cisd/tms/modules/mk/dto/UKeySignResult.java @@ -15,7 +15,9 @@ public class UKeySignResult { private String sign; /** - * 额外的数据,比如主密钥分量 + * 主密钥分量 */ - private String extra; + private String component; + + private String lmkMac; } diff --git a/src/main/java/com/cisd/tms/modules/mk/service/LmkService.java b/src/main/java/com/cisd/tms/modules/mk/service/LmkService.java index dafa85e..c1a4dbf 100644 --- a/src/main/java/com/cisd/tms/modules/mk/service/LmkService.java +++ b/src/main/java/com/cisd/tms/modules/mk/service/LmkService.java @@ -1,27 +1,24 @@ package com.cisd.tms.modules.mk.service; import com.cisd.tms.common.util.DivisionUtils; -import com.cisd.tms.common.util.SerializationUtil; import com.cisd.tms.integration.crypto.pcie.jna.EccRefPublicKey; import com.cisd.tms.integration.crypto.pcie.jna.EccSignature; import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; import com.cisd.tms.integration.crypto.pcie.model.EccInternalSignRequest; +import com.cisd.tms.integration.crypto.pcie.model.EccInternalVerifyRequest; import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.mk.config.MasterKeyInitProperties; import com.cisd.tms.modules.mk.common.LMKConstant; -import com.cisd.tms.modules.mk.dto.KeyComponent; -import com.cisd.tms.modules.mk.dto.LMK; -import com.cisd.tms.modules.mk.dto.LMKAndIK; +import com.cisd.tms.modules.mk.dto.MasterKeyBackupMaterial; +import com.cisd.tms.modules.mk.dto.MasterKeyStateResult; import com.cisd.tms.modules.mk.enums.IKEnums; import com.cisd.tms.modules.mk.enums.MasterKeyStatus; -import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.util.encoders.Hex; import org.springframework.stereotype.Service; -import java.io.IOException; -import java.io.StreamCorruptedException; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.HashMap; import java.util.List; @@ -35,90 +32,39 @@ import java.util.Map; @Service public class LmkService { -// private List keyComponentList; - - private List keyComponentList; - private static final int AUTH_SIGN_KEY_INDEX = IKEnums.KEY_TYPE_AUTH.getCode(); + private static final int LMK_MAC_BYTE_LENGTH = 8; + private final PcieCryptoService pcieCryptoService; + private final MasterKeyInitProperties masterKeyInitProperties; - @Resource - private PcieCryptoService pcieCryptoService; - - /** - * 生成主秘钥 - */ - public LMK generateLMK() { - // todo 生成主密钥前删除所有的对称和非对称密钥 - // this.delAllKey(); - - //生成主密钥 - pcieCryptoService.generateLmk(); - //生成内部密钥 - pcieCryptoService.generateIk(IKEnums.KEY_TYPE_AUTH.getCode()); - pcieCryptoService.generateIk(IKEnums.KEY_TYPE_DEVICE.getCode()); - try { - Thread.sleep(5000L); - } catch (InterruptedException e) { - log.error("sleep error"); - } - pcieCryptoService.loadLmk(); - try { - Thread.sleep(5000L); - } catch (InterruptedException e) { - log.error("sleep error"); - } - backUp(); -// iLmkService.generateLMKAndIK(null) -// .load() -// .backUp(0,0); - byte[] seedMac = pcieCryptoService.exportLmkSeedMac(); -// LmkKey lmkKey = soLibraryAdapter.getLmkKeySession(); - log.info("主密钥校验值:" + Hex.toHexString(seedMac)); - return LMK.getInstance(Hex.toHexString(seedMac)); + public LmkService(PcieCryptoService pcieCryptoService, MasterKeyInitProperties masterKeyInitProperties) { + this.pcieCryptoService = pcieCryptoService; + this.masterKeyInitProperties = masterKeyInitProperties; } + public MasterKeyStateResult initMasterKey() { + byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64()); + byte[] newPin = decodeConfiguredPin("tms.mk.init-identify.new-pin-base64", masterKeyInitProperties.getNewPinBase64()); - public void backUp() { - - BackupDataResult lmkEx = pcieCryptoService.backupLmkEx(56); - String lmk = Hex.toHexString(lmkEx.getData()); - - // todo 获取内部密钥 - BackupDataResult ikAuthEx = pcieCryptoService.backupLmkEx(56); - String authKey = Hex.toHexString(ikAuthEx.getData()); - BackupDataResult ikDeviceEx = pcieCryptoService.backupLmkEx(56); - String deviceKey = Hex.toHexString(ikDeviceEx.getData()); - -// BackupDataResult ikAuthEx = pcieCryptoService.backupIk(IKEnums.KEY_TYPE_AUTH.getCode()); -// String authKey = Hex.toHexString(ikAuthEx.getData()); -// BackupDataResult ikEx = pcieCryptoService.backupIk(IKEnums.KEY_TYPE_DEVICE.getCode()); -// String deviceKey = Hex.toHexString(ikEx .getData()); - - this.splitKey(lmk, authKey, deviceKey); + MasterKeyStateResult result = new MasterKeyStateResult(); + result.setStatus(true); + result.setSeedMac(Hex.toHexString(pcieCryptoService.initializeMasterKey(oldPin, newPin))); + return result; } - private void splitKey(String lmk,String authKey,String deviceKey){ - //平分内部密钥和主密钥并编号 - Map lmkComponentMap = new HashMap<>(); - Map authKeyComponentMap = new HashMap<>(); - Map deviceKeyComponentMap = new HashMap<>(); + public void destroyMasterKey() { + pcieCryptoService.destroyLmk(); + } - String[] lmkComponents = DivisionUtils.equalDivision(lmk, LMKConstant.COMPONENT_NUM); - String[] authKeyComponents = DivisionUtils.equalDivision(authKey,LMKConstant.COMPONENT_NUM); - String[] deviceKeyComponents = DivisionUtils.equalDivision(deviceKey,LMKConstant.COMPONENT_NUM); - //这里内部密钥和主密钥map的key是123,表示切分后按该顺序拼接 - for(int i = 0; i < LMKConstant.COMPONENT_NUM; i++){ - lmkComponentMap.put(LMKConstant.KEY_ORDER[i],lmkComponents[i]); - authKeyComponentMap.put(LMKConstant.KEY_ORDER[i],authKeyComponents[i]); - deviceKeyComponentMap.put(LMKConstant.KEY_ORDER[i],deviceKeyComponents[i]); + public MasterKeyStateResult queryMasterKeyStatus() { + MasterKeyStateResult result = new MasterKeyStateResult(); + boolean status = pcieCryptoService.checkLmk(); + result.setStatus(status); + if (status) { + result.setSeedMac(Hex.toHexString(pcieCryptoService.exportLmkSeedMac())); } - //组合分割后的主密钥和内部密钥 - - List keyComponentList = KeyComponent.getInstances(lmkComponentMap,authKeyComponentMap,deviceKeyComponentMap); - List result = new ArrayList<>(); - keyComponentList.forEach(k -> result.add(Base64.getEncoder().encodeToString(SerializationUtil.serializationObject(k)))); - this.keyComponentList = result; + return result; } public String exportIkPublicKeyHex() { @@ -136,77 +82,36 @@ public class LmkService { return Hex.toHexString(result.getData()); } - public String getComponent(int uid) { - if (keyComponentList == null || keyComponentList.isEmpty()) { - throw new IllegalStateException("key components not initialized"); - } - int index = uid - 1; - if (index < 0 || index >= keyComponentList.size()) { + public void verifyIk(String rawData, String signatureHex) { + EccInternalVerifyRequest request = new EccInternalVerifyRequest(); + request.setKeyIndex(AUTH_SIGN_KEY_INDEX); + request.setData(rawData.getBytes(StandardCharsets.UTF_8)); + request.setSignature(Hex.decode(signatureHex)); + pcieCryptoService.eccInternalVerify(request); + } + + public MasterKeyBackupMaterial getBackupMaterial(int uid) { + ParsedMasterKeyBackup parsedBackup = parseBackupMaterial(); + String component = parsedBackup.componentMap().get(uid); + if (component == null) { throw new IllegalArgumentException("invalid component index: " + uid); } - return keyComponentList.get(index); + return new MasterKeyBackupMaterial(component, parsedBackup.lmkMacHex()); } - public String masterKeyCompose(Map componentMap) { - StringBuilder lmk = new StringBuilder(); - for (int i = 0; i < LMKConstant.COMPONENT_NUM; i++) { - lmk.append(componentMap.get(String.valueOf(LMKConstant.KEY_ORDER[i]))); - } - return lmk.toString(); + public String getComponent(int uid) { + return getBackupMaterial(uid).getComponent(); } - public String recoveryLMK(List keyComponents) throws IOException, ClassNotFoundException { - List componentList = new ArrayList<>(); - for (String component : keyComponents) { - byte[] raw; - try { - raw = Base64.getDecoder().decode(component); - } catch (IllegalArgumentException e) { - StreamCorruptedException ex = new StreamCorruptedException("invalid component encoding"); - ex.initCause(e); - throw ex; - } - try { - Object obj = SerializationUtil.deserializationObject(raw); - componentList.add((KeyComponent) obj); - } catch (ClassCastException e) { - StreamCorruptedException ex = new StreamCorruptedException("invalid component payload"); - ex.initCause(e); - throw ex; - } catch (StreamCorruptedException e) { - throw e; - } - } - LMKAndIK lmkAndIK = LMKAndIK.getInstance(componentList); - pcieCryptoService.recoverLmkEx(Hex.decode(lmkAndIK.getLmk())); + public void recoverLmkEx(String lmkMacHex, List components) { + byte[] oldPin = decodeConfiguredPin("tms.mk.init-identify.old-pin-base64", masterKeyInitProperties.getOldPinBase64()); + byte[] newPin = decodeConfiguredPin("tms.mk.init-identify.new-pin-base64", masterKeyInitProperties.getNewPinBase64()); + String fullLmkHex = masterKeyCompose(lmkMacHex, components); + pcieCryptoService.initIdentify(oldPin, newPin); + pcieCryptoService.recoverLmkEx(Hex.decode(fullLmkHex)); pcieCryptoService.loadLmk(); - byte[] seedMac = pcieCryptoService.exportLmkSeedMac(); - return Hex.toHexString(seedMac); } - public String recoveryLMKAndGenIK(String lmkComponentHex) { - pcieCryptoService.recoverLmkEx(Hex.decode(lmkComponentHex)); - pcieCryptoService.generateIk(IKEnums.KEY_TYPE_AUTH.getCode()); - pcieCryptoService.generateIk(IKEnums.KEY_TYPE_DEVICE.getCode()); - pcieCryptoService.loadLmk(); - byte[] seedMac = pcieCryptoService.exportLmkSeedMac(); - return Hex.toHexString(seedMac); - } - - public LMK recoveryLMK1(LMK lmk) { - pcieCryptoService.recoverLmkEx(Hex.decode(lmk.getPcLmk())); - pcieCryptoService.loadLmk(); - byte[] seedMac = pcieCryptoService.exportLmkSeedMac(); - lmk.setUcSessionKey(Hex.toHexString(seedMac)); - return lmk; - } - - public void destroyLMKAndIK() { - pcieCryptoService.destroyLmk(); - pcieCryptoService.destroyIk(IKEnums.KEY_TYPE_AUTH.getCode()); - pcieCryptoService.destroyIk(IKEnums.KEY_TYPE_DEVICE.getCode()); - keyComponentList = null; - } public MasterKeyStatus.StatusDetail getMasterKeyStatus() { boolean ok = pcieCryptoService.checkLmk(); @@ -218,4 +123,63 @@ public class LmkService { return status.getDetail(); } + private String masterKeyCompose(String lmkMacHex, List components) { + if (lmkMacHex == null || lmkMacHex.isBlank()) { + throw new IllegalArgumentException("lmkMac must not be blank"); + } + if (components == null || components.size() != LMKConstant.COMPONENT_NUM) { + throw new IllegalArgumentException("master key components size must be " + LMKConstant.COMPONENT_NUM); + } + StringBuilder lmk = new StringBuilder(lmkMacHex); + for (int index = 0; index < LMKConstant.COMPONENT_NUM; index++) { + String component = components.get(index); + if (component == null || component.isBlank()) { + throw new IllegalArgumentException("master key component missing: " + (index + 1)); + } + lmk.append(component); + } + return lmk.toString(); + } + + private ParsedMasterKeyBackup parseBackupMaterial() { + //todo 需要将主密钥分量、内部密钥分量一起拆分后进行备份,密码卡暂时没有导出内部密钥分量接口 + BackupDataResult backupData = pcieCryptoService.backupLmkEx(); + byte[] normalized = normalizeBackupData(backupData); + if (normalized.length < LMK_MAC_BYTE_LENGTH) { + throw new IllegalStateException("backup lmk data length is too short: " + normalized.length); + } + byte[] lmkMac = Arrays.copyOfRange(normalized, 0, LMK_MAC_BYTE_LENGTH); + byte[] componentBytes = Arrays.copyOfRange(normalized, LMK_MAC_BYTE_LENGTH, normalized.length); + String[] splitComponents = DivisionUtils.equalDivision(Hex.toHexString(componentBytes), LMKConstant.COMPONENT_NUM); + Map componentMap = new HashMap<>(); + for (int i = 0; i < LMKConstant.COMPONENT_NUM; i++) { + componentMap.put(LMKConstant.KEY_ORDER[i], splitComponents[i]); + } + return new ParsedMasterKeyBackup(Hex.toHexString(lmkMac), componentMap); + } + + private byte[] normalizeBackupData(BackupDataResult backupData) { + byte[] data = backupData.getData(); + if (data == null || data.length == 0) { + return new byte[0]; + } + int length = backupData.getLength(); + int safeLength = length > 0 && length <= data.length ? length : data.length; + return Arrays.copyOf(data, safeLength); + } + + private byte[] decodeConfiguredPin(String fieldName, String base64Value) { + if (base64Value == null || base64Value.isBlank()) { + throw new IllegalStateException(fieldName + " is not configured"); + } + try { + return Base64.getDecoder().decode(base64Value); + } catch (IllegalArgumentException e) { + throw new IllegalStateException(fieldName + " is not valid Base64", e); + } + } + + private record ParsedMasterKeyBackup(String lmkMacHex, Map componentMap) { + } + } diff --git a/src/main/java/com/cisd/tms/modules/upgrade/config/UpgradeProperties.java b/src/main/java/com/cisd/tms/modules/upgrade/config/UpgradeProperties.java new file mode 100644 index 0000000..5c4fae0 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/config/UpgradeProperties.java @@ -0,0 +1,35 @@ +package com.cisd.tms.modules.upgrade.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "tms.upgrade") +public class UpgradeProperties { + + private String stagingRootDir = "/home/tmp/tms-upgrade-staging"; + private String logDir = "/home/tms/tmp/tms-upgrade-logs"; + private String signaturePublicKeyBase64 = ""; + + public String getStagingRootDir() { + return stagingRootDir; + } + + public void setStagingRootDir(String stagingRootDir) { + this.stagingRootDir = stagingRootDir; + } + + public String getLogDir() { + return logDir; + } + + public void setLogDir(String logDir) { + this.logDir = logDir; + } + + public String getSignaturePublicKeyBase64() { + return signaturePublicKeyBase64; + } + + public void setSignaturePublicKeyBase64(String signaturePublicKeyBase64) { + this.signaturePublicKeyBase64 = signaturePublicKeyBase64; + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/controller/UpgradeController.java b/src/main/java/com/cisd/tms/modules/upgrade/controller/UpgradeController.java new file mode 100644 index 0000000..e4040a6 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/controller/UpgradeController.java @@ -0,0 +1,98 @@ +package com.cisd.tms.modules.upgrade.controller; + +import com.cisd.tms.common.api.ApiResponse; +import com.cisd.tms.modules.file.dto.FileUploadResponse; +import com.cisd.tms.modules.file.service.FileService; +import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest; +import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradeLogResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradePreviewRequest; +import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradeTaskPageResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradeTaskResponse; +import com.cisd.tms.modules.upgrade.service.UpgradeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/api/v1") +@Tag(name = "离线升级管理", description = "离线升级包上传、预检、任务创建、执行和升级记录查询接口") +public class UpgradeController { + + private final UpgradeService upgradeService; + private final FileService fileService; + + public UpgradeController(UpgradeService upgradeService, FileService fileService) { + this.upgradeService = upgradeService; + this.fileService = fileService; + } + + @PostMapping("/upgrade-packages") + @Operation(summary = "上传离线升级包", description = "上传离线升级包,返回 fileId 供升级预检和任务创建使用。") + public ApiResponse uploadPackage( + @Parameter(description = "待上传的离线升级包") + @RequestParam("file") MultipartFile file + ) { + return ApiResponse.success(fileService.upload(file)); + } + + @PostMapping("/upgrades/previews") + @Operation(summary = "预检离线升级包", description = "根据 fileId 解析升级包,校验摘要和签名,并返回版本变化和是否允许升级。") + public ApiResponse preview(@Valid @RequestBody UpgradePreviewRequest request) { + return ApiResponse.success(upgradeService.preview(request)); + } + + @PostMapping("/upgrades") + @Operation(summary = "创建离线升级任务", description = "根据 fileId 创建离线升级任务,状态初始为 PENDING_CONFIRM。") + public ApiResponse create(@Valid @RequestBody UpgradeCreateTaskRequest request) { + return ApiResponse.success(upgradeService.createTask(request)); + } + + @PostMapping("/upgrades/{taskId}/execute") + @Operation(summary = "执行离线升级任务", description = "异步受理升级执行请求,立即返回任务当前快照。") + public ApiResponse execute( + @Parameter(description = "升级任务号") + @PathVariable String taskId + ) { + return ApiResponse.success(upgradeService.executeTask(taskId)); + } + + @GetMapping("/upgrades") + @Operation(summary = "查询升级记录列表", description = "分页查询升级任务列表,支持按升级对象和状态筛选。") + public ApiResponse list( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int pageSize, + @RequestParam(required = false) String targetComponent, + @RequestParam(required = false) String status + ) { + return ApiResponse.success(upgradeService.listTasks(page, pageSize, targetComponent, status)); + } + + @GetMapping("/upgrades/{taskId}") + @Operation(summary = "查询升级任务详情", description = "返回单个升级任务的状态、版本变化和日志路径等信息。") + public ApiResponse detail( + @Parameter(description = "升级任务号") + @PathVariable String taskId + ) { + return ApiResponse.success(upgradeService.getTask(taskId)); + } + + @GetMapping("/upgrades/{taskId}/log") + @Operation(summary = "查询升级任务日志", description = "读取升级任务的日志文件内容,返回升级执行日志。") + public ApiResponse log( + @Parameter(description = "升级任务号") + @PathVariable String taskId + ) { + return ApiResponse.success(upgradeService.getTaskLog(taskId)); + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeCreateTaskRequest.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeCreateTaskRequest.java new file mode 100644 index 0000000..e9f1939 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeCreateTaskRequest.java @@ -0,0 +1,18 @@ +package com.cisd.tms.modules.upgrade.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "创建离线升级任务请求") +public class UpgradeCreateTaskRequest { + + @Schema(description = "升级包文件标识", example = "FILE-20260319-000001", requiredMode = Schema.RequiredMode.REQUIRED) + private String fileId; + + public String getFileId() { + return fileId; + } + + public void setFileId(String fileId) { + this.fileId = fileId; + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeCreateTaskResponse.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeCreateTaskResponse.java new file mode 100644 index 0000000..2c4d644 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeCreateTaskResponse.java @@ -0,0 +1,58 @@ +package com.cisd.tms.modules.upgrade.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "创建离线升级任务响应") +public class UpgradeCreateTaskResponse { + + @Schema(description = "升级任务号", example = "UPG-20260319-000001") + private String taskId; + @Schema(description = "升级任务状态", example = "PENDING_CONFIRM") + private String status; + @Schema(description = "升级对象", example = "STANDARD_APP") + private String targetComponent; + @Schema(description = "当前版本", example = "V1.3.1") + private String currentVersion; + @Schema(description = "目标版本", example = "V1.3.2") + private String targetVersion; + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getTargetComponent() { + return targetComponent; + } + + public void setTargetComponent(String targetComponent) { + this.targetComponent = targetComponent; + } + + public String getCurrentVersion() { + return currentVersion; + } + + public void setCurrentVersion(String currentVersion) { + this.currentVersion = currentVersion; + } + + public String getTargetVersion() { + return targetVersion; + } + + public void setTargetVersion(String targetVersion) { + this.targetVersion = targetVersion; + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeLogResponse.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeLogResponse.java new file mode 100644 index 0000000..3d1dd8a --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeLogResponse.java @@ -0,0 +1,38 @@ +package com.cisd.tms.modules.upgrade.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "升级任务日志响应") +public class UpgradeLogResponse { + + @Schema(description = "升级任务号", example = "UPG-20260319093000-ABC123") + private String taskId; + @Schema(description = "日志文件路径", example = "/home/tms/tmp/tms-upgrade-logs/UPG-20260319093000-ABC123.log") + private String logPath; + @Schema(description = "日志内容") + private String content; + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public String getLogPath() { + return logPath; + } + + public void setLogPath(String logPath) { + this.logPath = logPath; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java new file mode 100644 index 0000000..315a712 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeManifest.java @@ -0,0 +1,90 @@ +package com.cisd.tms.modules.upgrade.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "离线升级包 manifest 描述") +public class UpgradeManifest { + + @Schema(description = "升级包唯一标识", example = "PKG_20260319_001") + private String packageId; + @Schema(description = "升级对象", example = "STANDARD_APP") + private String targetComponent; + @Schema(description = "目标版本", example = "V1.3.2") + private String targetVersion; + @Schema(description = "兼容的最低旧版本", example = "V1.3.0") + private String minCompatibleVersion; + @Schema(description = "升级包描述", example = "修复标准收发器已知问题") + private String description; + @Schema(description = "升级执行脚本路径", example = "scripts/orchestrator.sh") + private String orchestrator; + @Schema(description = "升级包校验信息") + private FileChecksums fileChecksums = new FileChecksums(); + + public String getPackageId() { + return packageId; + } + + public void setPackageId(String packageId) { + this.packageId = packageId; + } + + public String getTargetComponent() { + return targetComponent; + } + + public void setTargetComponent(String targetComponent) { + this.targetComponent = targetComponent; + } + + public String getTargetVersion() { + return targetVersion; + } + + public void setTargetVersion(String targetVersion) { + this.targetVersion = targetVersion; + } + + public String getMinCompatibleVersion() { + return minCompatibleVersion; + } + + public void setMinCompatibleVersion(String minCompatibleVersion) { + this.minCompatibleVersion = minCompatibleVersion; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getOrchestrator() { + return orchestrator; + } + + public void setOrchestrator(String orchestrator) { + this.orchestrator = orchestrator; + } + + public FileChecksums getFileChecksums() { + return fileChecksums; + } + + public void setFileChecksums(FileChecksums fileChecksums) { + this.fileChecksums = fileChecksums; + } + + public static class FileChecksums { + private String payloadZip; + + public String getPayloadZip() { + return payloadZip; + } + + public void setPayloadZip(String payloadZip) { + this.payloadZip = payloadZip; + } + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradePreviewRequest.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradePreviewRequest.java new file mode 100644 index 0000000..d44b158 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradePreviewRequest.java @@ -0,0 +1,18 @@ +package com.cisd.tms.modules.upgrade.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "离线升级包预检请求") +public class UpgradePreviewRequest { + + @Schema(description = "升级包文件标识", example = "FILE-20260319-000001", requiredMode = Schema.RequiredMode.REQUIRED) + private String fileId; + + public String getFileId() { + return fileId; + } + + public void setFileId(String fileId) { + this.fileId = fileId; + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradePreviewResponse.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradePreviewResponse.java new file mode 100644 index 0000000..34ee50f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradePreviewResponse.java @@ -0,0 +1,98 @@ +package com.cisd.tms.modules.upgrade.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "离线升级包预检响应") +public class UpgradePreviewResponse { + + @Schema(description = "升级对象", example = "STANDARD_APP") + private String targetComponent; + @Schema(description = "当前版本", example = "V1.3.1") + private String currentVersion; + @Schema(description = "目标版本", example = "V1.3.2") + private String targetVersion; + @Schema(description = "版本变化提示", example = "标准收发器企业版将从 V1.3.1 升级到 V1.3.2") + private String versionChangeSummary; + @Schema(description = "签名与摘要校验是否通过", example = "true") + private boolean verifyPassed; + @Schema(description = "预检信息", example = "校验通过") + private String verifyMessage; + @Schema(description = "是否允许创建升级任务", example = "true") + private boolean allowed; + @Schema(description = "阻断原因", example = "目标版本低于当前版本") + private String blockReason; + @Schema(description = "升级包说明", example = "修复标准收发器已知问题") + private String description; + + public String getTargetComponent() { + return targetComponent; + } + + public void setTargetComponent(String targetComponent) { + this.targetComponent = targetComponent; + } + + public String getCurrentVersion() { + return currentVersion; + } + + public void setCurrentVersion(String currentVersion) { + this.currentVersion = currentVersion; + } + + public String getTargetVersion() { + return targetVersion; + } + + public void setTargetVersion(String targetVersion) { + this.targetVersion = targetVersion; + } + + public String getVersionChangeSummary() { + return versionChangeSummary; + } + + public void setVersionChangeSummary(String versionChangeSummary) { + this.versionChangeSummary = versionChangeSummary; + } + + public boolean isVerifyPassed() { + return verifyPassed; + } + + public void setVerifyPassed(boolean verifyPassed) { + this.verifyPassed = verifyPassed; + } + + public String getVerifyMessage() { + return verifyMessage; + } + + public void setVerifyMessage(String verifyMessage) { + this.verifyMessage = verifyMessage; + } + + public boolean isAllowed() { + return allowed; + } + + public void setAllowed(boolean allowed) { + this.allowed = allowed; + } + + public String getBlockReason() { + return blockReason; + } + + public void setBlockReason(String blockReason) { + this.blockReason = blockReason; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeTaskPageResponse.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeTaskPageResponse.java new file mode 100644 index 0000000..6b696b5 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeTaskPageResponse.java @@ -0,0 +1,50 @@ +package com.cisd.tms.modules.upgrade.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "升级任务分页响应") +public class UpgradeTaskPageResponse { + + @Schema(description = "页码,从 1 开始", example = "1") + private int page; + @Schema(description = "每页大小", example = "10") + private int pageSize; + @Schema(description = "总记录数", example = "23") + private long total; + @Schema(description = "任务列表") + private List items = new ArrayList<>(); + + public int getPage() { + return page; + } + + public void setPage(int page) { + this.page = page; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public long getTotal() { + return total; + } + + public void setTotal(long total) { + this.total = total; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeTaskResponse.java b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeTaskResponse.java new file mode 100644 index 0000000..c90bdd8 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/dto/UpgradeTaskResponse.java @@ -0,0 +1,138 @@ +package com.cisd.tms.modules.upgrade.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "升级任务详情响应") +public class UpgradeTaskResponse { + + @Schema(description = "升级任务号", example = "UPG-20260319093000-ABC123") + private String taskId; + @Schema(description = "升级方式", example = "OFFLINE") + private String upgradeMode; + @Schema(description = "升级对象", example = "STANDARD_APP") + private String targetComponent; + @Schema(description = "升级包文件标识", example = "FILE-20260319-000001") + private String packageFileId; + @Schema(description = "升级包名称", example = "upgrade-package.zip") + private String packageName; + @Schema(description = "升级前版本", example = "V1.3.1") + private String currentVersion; + @Schema(description = "升级目标版本", example = "V1.3.2") + private String targetVersion; + @Schema(description = "任务状态", example = "PENDING_CONFIRM") + private String status; + @Schema(description = "预检信息", example = "校验通过") + private String verifyMessage; + @Schema(description = "错误信息", example = "orchestrator exited with 1") + private String errorMessage; + @Schema(description = "日志文件路径", example = "/home/tms/tmp/tms-upgrade-logs/UPG-20260319093000-ABC123.log") + private String detailLogPath; + @Schema(description = "开始时间", example = "2026-03-19T09:30:00") + private String startedAt; + @Schema(description = "完成时间", example = "2026-03-19T09:35:00") + private String finishedAt; + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public String getUpgradeMode() { + return upgradeMode; + } + + public void setUpgradeMode(String upgradeMode) { + this.upgradeMode = upgradeMode; + } + + public String getTargetComponent() { + return targetComponent; + } + + public void setTargetComponent(String targetComponent) { + this.targetComponent = targetComponent; + } + + public String getPackageFileId() { + return packageFileId; + } + + public void setPackageFileId(String packageFileId) { + this.packageFileId = packageFileId; + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getCurrentVersion() { + return currentVersion; + } + + public void setCurrentVersion(String currentVersion) { + this.currentVersion = currentVersion; + } + + public String getTargetVersion() { + return targetVersion; + } + + public void setTargetVersion(String targetVersion) { + this.targetVersion = targetVersion; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getVerifyMessage() { + return verifyMessage; + } + + public void setVerifyMessage(String verifyMessage) { + this.verifyMessage = verifyMessage; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getDetailLogPath() { + return detailLogPath; + } + + public void setDetailLogPath(String detailLogPath) { + this.detailLogPath = detailLogPath; + } + + public String getStartedAt() { + return startedAt; + } + + public void setStartedAt(String startedAt) { + this.startedAt = startedAt; + } + + public String getFinishedAt() { + return finishedAt; + } + + public void setFinishedAt(String finishedAt) { + this.finishedAt = finishedAt; + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/entity/UpgradeTaskEntity.java b/src/main/java/com/cisd/tms/modules/upgrade/entity/UpgradeTaskEntity.java new file mode 100644 index 0000000..fce08e9 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/entity/UpgradeTaskEntity.java @@ -0,0 +1,163 @@ +package com.cisd.tms.modules.upgrade.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.cisd.tms.infrastructure.persistence.entity.BaseEntity; +import java.time.LocalDateTime; + +@TableName("t_upgrade_task") +public class UpgradeTaskEntity extends BaseEntity { + + private String taskId; + private String upgradeMode; + private String targetComponent; + private String packageFileId; + private String packageName; + private String packageHash; + private String currentVersion; + private String targetVersion; + private String status; + private Boolean verifyPassed; + private String verifyMessage; + private String manifestJson; + private String detailLogPath; + private String errorMessage; + private String operator; + private LocalDateTime startedAt; + private LocalDateTime finishedAt; + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public String getUpgradeMode() { + return upgradeMode; + } + + public void setUpgradeMode(String upgradeMode) { + this.upgradeMode = upgradeMode; + } + + public String getTargetComponent() { + return targetComponent; + } + + public void setTargetComponent(String targetComponent) { + this.targetComponent = targetComponent; + } + + public String getPackageFileId() { + return packageFileId; + } + + public void setPackageFileId(String packageFileId) { + this.packageFileId = packageFileId; + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getPackageHash() { + return packageHash; + } + + public void setPackageHash(String packageHash) { + this.packageHash = packageHash; + } + + public String getCurrentVersion() { + return currentVersion; + } + + public void setCurrentVersion(String currentVersion) { + this.currentVersion = currentVersion; + } + + public String getTargetVersion() { + return targetVersion; + } + + public void setTargetVersion(String targetVersion) { + this.targetVersion = targetVersion; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Boolean getVerifyPassed() { + return verifyPassed; + } + + public void setVerifyPassed(Boolean verifyPassed) { + this.verifyPassed = verifyPassed; + } + + public String getVerifyMessage() { + return verifyMessage; + } + + public void setVerifyMessage(String verifyMessage) { + this.verifyMessage = verifyMessage; + } + + public String getManifestJson() { + return manifestJson; + } + + public void setManifestJson(String manifestJson) { + this.manifestJson = manifestJson; + } + + public String getDetailLogPath() { + return detailLogPath; + } + + public void setDetailLogPath(String detailLogPath) { + this.detailLogPath = detailLogPath; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getOperator() { + return operator; + } + + public void setOperator(String operator) { + this.operator = operator; + } + + public LocalDateTime getStartedAt() { + return startedAt; + } + + public void setStartedAt(LocalDateTime startedAt) { + this.startedAt = startedAt; + } + + public LocalDateTime getFinishedAt() { + return finishedAt; + } + + public void setFinishedAt(LocalDateTime finishedAt) { + this.finishedAt = finishedAt; + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTargetComponent.java b/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTargetComponent.java new file mode 100644 index 0000000..259b7e5 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTargetComponent.java @@ -0,0 +1,7 @@ +package com.cisd.tms.modules.upgrade.enums; + +public enum UpgradeTargetComponent { + TMS, + STANDARD_APP, + DIRECT_APP +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTaskStatus.java b/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTaskStatus.java new file mode 100644 index 0000000..56d1343 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/enums/UpgradeTaskStatus.java @@ -0,0 +1,8 @@ +package com.cisd.tms.modules.upgrade.enums; + +public enum UpgradeTaskStatus { + PENDING_CONFIRM, + RUNNING, + SUCCESS, + FAILED +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java b/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java new file mode 100644 index 0000000..0799e08 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/executor/UpgradeTaskRunner.java @@ -0,0 +1,132 @@ +package com.cisd.tms.modules.upgrade.executor; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.file.entity.FileRecordEntity; +import com.cisd.tms.modules.file.repository.FileRecordRepository; +import com.cisd.tms.modules.upgrade.config.UpgradeProperties; +import com.cisd.tms.modules.upgrade.dto.UpgradeManifest; +import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity; +import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository; +import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import org.springframework.stereotype.Component; + +@Component +public class UpgradeTaskRunner { + + private final UpgradeTaskRepository upgradeTaskRepository; + private final FileRecordRepository fileRecordRepository; + private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository; + private final UpgradePackageStagingService upgradePackageStagingService; + private final UpgradeProperties upgradeProperties; + private final ObjectMapper objectMapper; + + public UpgradeTaskRunner( + UpgradeTaskRepository upgradeTaskRepository, + FileRecordRepository fileRecordRepository, + DeviceSoftwareVersionRepository deviceSoftwareVersionRepository, + UpgradePackageStagingService upgradePackageStagingService, + UpgradeProperties upgradeProperties, + ObjectMapper objectMapper + ) { + this.upgradeTaskRepository = upgradeTaskRepository; + this.fileRecordRepository = fileRecordRepository; + this.deviceSoftwareVersionRepository = deviceSoftwareVersionRepository; + this.upgradePackageStagingService = upgradePackageStagingService; + this.upgradeProperties = upgradeProperties; + this.objectMapper = objectMapper; + } + + public void run(UpgradeTaskEntity task) { + LocalDateTime startedAt = LocalDateTime.now(); + Path logPath = logPath(task.getTaskId()); + try { + Files.createDirectories(logPath.getParent()); + Files.writeString(logPath, ""); + + FileRecordEntity record = fileRecordRepository.findByFileId(task.getPackageFileId()) + .orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + task.getPackageFileId())); + Path stagedDir = upgradePackageStagingService.stage(task.getTaskId(), Path.of(record.getStoragePath())); + UpgradeManifest manifest = objectMapper.readValue(Files.readString(stagedDir.resolve("manifest.json")), UpgradeManifest.class); + Path payloadDir = upgradePackageStagingService.stage(task.getTaskId() + "-payload", stagedDir.resolve("payload.zip")); + Path orchestrator = payloadDir.resolve(manifest.getOrchestrator()).normalize().toAbsolutePath(); + if (!Files.exists(orchestrator)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade orchestrator not found"); + } + + Process process = new ProcessBuilder("bash", orchestrator.toString(), task.getTaskId()) + .directory(payloadDir.toFile()) + .redirectErrorStream(true) + .redirectOutput(logPath.toFile()) + .start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + upgradeTaskRepository.updateStatus( + task.getTaskId(), + "FAILED", + "orchestrator exited with " + exitCode, + logPath.toString(), + startedAt, + LocalDateTime.now() + ); + return; + } + + DeviceSoftwareVersionEntity versionEntity = new DeviceSoftwareVersionEntity(); + versionEntity.setComponentCode(task.getTargetComponent()); + versionEntity.setComponentName(componentName(task.getTargetComponent())); + versionEntity.setCurrentVersion(task.getTargetVersion()); + versionEntity.setSourceType("UPGRADE"); + versionEntity.setDetectedAt(LocalDateTime.now()); + versionEntity.setRemarks("updated by offline upgrade task " + task.getTaskId()); + deviceSoftwareVersionRepository.saveOrUpdate(versionEntity); + + upgradeTaskRepository.updateStatus( + task.getTaskId(), + "SUCCESS", + "", + logPath.toString(), + startedAt, + LocalDateTime.now() + ); + } catch (Exception ex) { + try { + Files.createDirectories(logPath.getParent()); + Files.writeString(logPath, ex.getMessage() == null ? "upgrade failed" : ex.getMessage()); + } catch (IOException ignored) { + } + upgradeTaskRepository.updateStatus( + task.getTaskId(), + "FAILED", + ex.getMessage(), + logPath.toString(), + startedAt, + LocalDateTime.now() + ); + } + } + + private Path logPath(String taskId) { + return Path.of(trim(upgradeProperties.getLogDir())).normalize().toAbsolutePath().resolve(taskId + ".log"); + } + + private String componentName(String componentCode) { + return switch (trim(componentCode)) { + case "TMS" -> "设备管理软件"; + case "STANDARD_APP" -> "标准收发器"; + case "DIRECT_APP" -> "直参轻量化终端"; + default -> componentCode; + }; + } + + private String trim(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/mapper/UpgradeTaskMapper.java b/src/main/java/com/cisd/tms/modules/upgrade/mapper/UpgradeTaskMapper.java new file mode 100644 index 0000000..d0327f1 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/mapper/UpgradeTaskMapper.java @@ -0,0 +1,37 @@ +package com.cisd.tms.modules.upgrade.mapper; + +import com.cisd.tms.infrastructure.persistence.mapper.BaseMapperX; +import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity; +import java.time.LocalDateTime; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UpgradeTaskMapper extends BaseMapperX { + + UpgradeTaskEntity selectByTaskId(@Param("taskId") String taskId); + + UpgradeTaskEntity selectRunningTask(); + + List selectPage( + @Param("targetComponent") String targetComponent, + @Param("status") String status, + @Param("offset") int offset, + @Param("limit") int limit + ); + + long countPage( + @Param("targetComponent") String targetComponent, + @Param("status") String status + ); + + int updateStatusByTaskId( + @Param("taskId") String taskId, + @Param("status") String status, + @Param("errorMessage") String errorMessage, + @Param("detailLogPath") String detailLogPath, + @Param("startedAt") LocalDateTime startedAt, + @Param("finishedAt") LocalDateTime finishedAt + ); +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepository.java b/src/main/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepository.java new file mode 100644 index 0000000..31e0694 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepository.java @@ -0,0 +1,28 @@ +package com.cisd.tms.modules.upgrade.repository; + +import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +public interface UpgradeTaskRepository { + + void save(UpgradeTaskEntity entity); + + Optional findByTaskId(String taskId); + + Optional findRunningTask(); + + List findPage(String targetComponent, String status, int offset, int limit); + + long countPage(String targetComponent, String status); + + void updateStatus( + String taskId, + String status, + String errorMessage, + String detailLogPath, + LocalDateTime startedAt, + LocalDateTime finishedAt + ); +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/repository/impl/UpgradeTaskRepositoryImpl.java b/src/main/java/com/cisd/tms/modules/upgrade/repository/impl/UpgradeTaskRepositoryImpl.java new file mode 100644 index 0000000..58bbd5f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/repository/impl/UpgradeTaskRepositoryImpl.java @@ -0,0 +1,56 @@ +package com.cisd.tms.modules.upgrade.repository.impl; + +import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity; +import com.cisd.tms.modules.upgrade.mapper.UpgradeTaskMapper; +import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import org.springframework.stereotype.Repository; + +@Repository +public class UpgradeTaskRepositoryImpl implements UpgradeTaskRepository { + + private final UpgradeTaskMapper upgradeTaskMapper; + + public UpgradeTaskRepositoryImpl(UpgradeTaskMapper upgradeTaskMapper) { + this.upgradeTaskMapper = upgradeTaskMapper; + } + + @Override + public void save(UpgradeTaskEntity entity) { + upgradeTaskMapper.insert(entity); + } + + @Override + public Optional findByTaskId(String taskId) { + return Optional.ofNullable(upgradeTaskMapper.selectByTaskId(taskId)); + } + + @Override + public Optional findRunningTask() { + return Optional.ofNullable(upgradeTaskMapper.selectRunningTask()); + } + + @Override + public List findPage(String targetComponent, String status, int offset, int limit) { + return upgradeTaskMapper.selectPage(targetComponent, status, offset, limit); + } + + @Override + public long countPage(String targetComponent, String status) { + return upgradeTaskMapper.countPage(targetComponent, status); + } + + @Override + public void updateStatus( + String taskId, + String status, + String errorMessage, + String detailLogPath, + LocalDateTime startedAt, + LocalDateTime finishedAt + ) { + upgradeTaskMapper.updateStatusByTaskId(taskId, status, errorMessage, detailLogPath, startedAt, finishedAt); + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java b/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java new file mode 100644 index 0000000..c069555 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradePackageService.java @@ -0,0 +1,151 @@ +package com.cisd.tms.modules.upgrade.service; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.file.entity.FileRecordEntity; +import com.cisd.tms.modules.file.repository.FileRecordRepository; +import com.cisd.tms.modules.upgrade.dto.UpgradeManifest; +import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse; +import com.cisd.tms.modules.upgrade.support.UpgradePackageSignatureVerifier; +import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService; +import com.cisd.tms.modules.upgrade.support.UpgradeVersionComparator; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Optional; +import org.springframework.stereotype.Service; + +@Service +public class UpgradePackageService { + + private final FileRecordRepository fileRecordRepository; + private final DeviceSoftwareVersionRepository deviceSoftwareVersionRepository; + private final UpgradePackageStagingService upgradePackageStagingService; + private final UpgradePackageSignatureVerifier signatureVerifier; + private final UpgradeVersionComparator versionComparator; + private final ObjectMapper objectMapper; + + public UpgradePackageService( + FileRecordRepository fileRecordRepository, + DeviceSoftwareVersionRepository deviceSoftwareVersionRepository, + UpgradePackageStagingService upgradePackageStagingService, + UpgradePackageSignatureVerifier signatureVerifier, + UpgradeVersionComparator versionComparator, + ObjectMapper objectMapper + ) { + this.fileRecordRepository = fileRecordRepository; + this.deviceSoftwareVersionRepository = deviceSoftwareVersionRepository; + this.upgradePackageStagingService = upgradePackageStagingService; + this.signatureVerifier = signatureVerifier; + this.versionComparator = versionComparator; + this.objectMapper = objectMapper; + } + + public UpgradePreviewResponse preview(String fileId) { + FileRecordEntity record = fileRecordRepository.findByFileId(normalize(fileId)) + .orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + normalize(fileId))); + Path packagePath = Path.of(record.getStoragePath()).normalize().toAbsolutePath(); + if (!Files.exists(packagePath)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found on disk"); + } + + Path stagedDir = upgradePackageStagingService.stage(record.getFileId(), packagePath); + Path manifestPath = stagedDir.resolve("manifest.json"); + Path payloadZipPath = stagedDir.resolve("payload.zip"); + Path signaturePath = stagedDir.resolve("signature.sig"); + + if (!Files.exists(manifestPath) || !Files.exists(payloadZipPath)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package structure invalid"); + } + + UpgradeManifest manifest = readManifest(manifestPath); + verifyPayloadChecksum(manifest, payloadZipPath); + signatureVerifier.verify(manifest, manifestPath, payloadZipPath, signaturePath); + + String currentVersion = currentVersion(manifest.getTargetComponent()); + String targetVersion = normalize(manifest.getTargetVersion()); + + if (!currentVersion.isEmpty() && versionComparator.compare(targetVersion, currentVersion) < 0) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "target version is lower than current version"); + } + String minCompatibleVersion = normalize(manifest.getMinCompatibleVersion()); + if (!currentVersion.isEmpty() && !minCompatibleVersion.isEmpty() && versionComparator.compare(currentVersion, minCompatibleVersion) < 0) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "current version does not satisfy minimum compatible version"); + } + + UpgradePreviewResponse response = new UpgradePreviewResponse(); + response.setTargetComponent(normalize(manifest.getTargetComponent())); + response.setCurrentVersion(currentVersion); + response.setTargetVersion(targetVersion); + response.setVersionChangeSummary(buildVersionSummary(manifest.getTargetComponent(), currentVersion, targetVersion)); + response.setVerifyPassed(true); + response.setVerifyMessage("校验通过"); + response.setAllowed(true); + response.setBlockReason(""); + response.setDescription(normalize(manifest.getDescription())); + return response; + } + + private UpgradeManifest readManifest(Path manifestPath) { + try { + return objectMapper.readValue(Files.readString(manifestPath), UpgradeManifest.class); + } catch (IOException ex) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "manifest.json is invalid"); + } + } + + private void verifyPayloadChecksum(UpgradeManifest manifest, Path payloadZipPath) { + String expected = normalize(manifest.getFileChecksums() == null ? "" : manifest.getFileChecksums().getPayloadZip()); + if (expected.isEmpty()) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "payload checksum missing"); + } + String actual = sha256Hex(payloadZipPath); + if (!expected.equalsIgnoreCase(actual)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "payload checksum mismatch"); + } + } + + private String sha256Hex(Path file) { + try (InputStream inputStream = Files.newInputStream(file)) { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] buffer = new byte[8192]; + int read; + while ((read = inputStream.read(buffer)) >= 0) { + if (read > 0) { + digest.update(buffer, 0, read); + } + } + return HexFormat.of().formatHex(digest.digest()); + } catch (IOException | NoSuchAlgorithmException ex) { + throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to calculate payload checksum"); + } + } + + private String currentVersion(String componentCode) { + Optional cached = deviceSoftwareVersionRepository.findByComponentCode(normalize(componentCode)); + return cached.map(DeviceSoftwareVersionEntity::getCurrentVersion).map(this::normalize).orElse(""); + } + + private String buildVersionSummary(String componentCode, String currentVersion, String targetVersion) { + String label = switch (normalize(componentCode)) { + case "TMS" -> "设备管理软件"; + case "STANDARD_APP" -> "标准收发器"; + case "DIRECT_APP" -> "直参轻量化终端"; + default -> "目标组件"; + }; + String from = currentVersion.isEmpty() ? "未记录版本" : currentVersion; + String to = targetVersion.isEmpty() ? "未知目标版本" : targetVersion; + return label + "将从 " + from + " 升级到 " + to; + } + + private String normalize(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradeService.java b/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradeService.java new file mode 100644 index 0000000..3883873 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/service/UpgradeService.java @@ -0,0 +1,232 @@ +package com.cisd.tms.modules.upgrade.service; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.file.entity.FileRecordEntity; +import com.cisd.tms.modules.file.repository.FileRecordRepository; +import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest; +import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradeLogResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradePreviewRequest; +import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradeTaskPageResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradeTaskResponse; +import com.cisd.tms.modules.upgrade.config.UpgradeProperties; +import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity; +import com.cisd.tms.modules.upgrade.executor.UpgradeTaskRunner; +import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class UpgradeService { + + private static final DateTimeFormatter TASK_ID_TS = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + private final UpgradeTaskRepository upgradeTaskRepository; + private final FileRecordRepository fileRecordRepository; + private final UpgradePackageService upgradePackageService; + private final UpgradeTaskRunner upgradeTaskRunner; + private final UpgradeProperties upgradeProperties; + private final Executor backgroundExecutor; + private final ConcurrentMap> activeExecutions; + + @Autowired + public UpgradeService( + UpgradeTaskRepository upgradeTaskRepository, + FileRecordRepository fileRecordRepository, + UpgradePackageService upgradePackageService, + UpgradeTaskRunner upgradeTaskRunner, + UpgradeProperties upgradeProperties + ) { + this( + upgradeTaskRepository, + fileRecordRepository, + upgradePackageService, + upgradeTaskRunner, + upgradeProperties, + Executors.newCachedThreadPool(), + new ConcurrentHashMap<>() + ); + } + + UpgradeService( + UpgradeTaskRepository upgradeTaskRepository, + FileRecordRepository fileRecordRepository, + UpgradePackageService upgradePackageService, + UpgradeTaskRunner upgradeTaskRunner, + UpgradeProperties upgradeProperties, + Executor backgroundExecutor, + ConcurrentMap> activeExecutions + ) { + this.upgradeTaskRepository = upgradeTaskRepository; + this.fileRecordRepository = fileRecordRepository; + this.upgradePackageService = upgradePackageService; + this.upgradeTaskRunner = upgradeTaskRunner; + this.upgradeProperties = upgradeProperties; + this.backgroundExecutor = backgroundExecutor; + this.activeExecutions = activeExecutions; + } + + public UpgradePreviewResponse preview(UpgradePreviewRequest request) { + if (request == null || request.getFileId() == null || request.getFileId().isBlank()) { + throw new IllegalArgumentException("fileId must not be blank"); + } + return upgradePackageService.preview(request.getFileId()); + } + + public UpgradeCreateTaskResponse createTask(UpgradeCreateTaskRequest request) { + if (request == null || request.getFileId() == null || request.getFileId().isBlank()) { + throw new IllegalArgumentException("fileId must not be blank"); + } + UpgradePreviewResponse preview = upgradePackageService.preview(request.getFileId()); + if (!preview.isAllowed()) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), blankAs(preview.getBlockReason(), "upgrade preview not allowed")); + } + FileRecordEntity fileRecord = fileRecordRepository.findByFileId(request.getFileId().trim()) + .orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package file not found: " + request.getFileId().trim())); + + UpgradeTaskEntity entity = new UpgradeTaskEntity(); + entity.setTaskId(buildTaskId()); + entity.setUpgradeMode("OFFLINE"); + entity.setTargetComponent(preview.getTargetComponent()); + entity.setPackageFileId(request.getFileId().trim()); + entity.setPackageName(fileRecord.getOriginalFilename()); + entity.setPackageHash(""); + entity.setCurrentVersion(preview.getCurrentVersion()); + entity.setTargetVersion(preview.getTargetVersion()); + entity.setStatus("PENDING_CONFIRM"); + entity.setVerifyPassed(preview.isVerifyPassed()); + entity.setVerifyMessage(preview.getVerifyMessage()); + entity.setManifestJson(""); + entity.setOperator(""); + entity.setCreateTime(LocalDateTime.now()); + entity.setUpdateTime(LocalDateTime.now()); + upgradeTaskRepository.save(entity); + + UpgradeCreateTaskResponse response = new UpgradeCreateTaskResponse(); + response.setTaskId(entity.getTaskId()); + response.setStatus(entity.getStatus()); + response.setTargetComponent(entity.getTargetComponent()); + response.setCurrentVersion(entity.getCurrentVersion()); + response.setTargetVersion(entity.getTargetVersion()); + return response; + } + + public UpgradeTaskResponse executeTask(String taskId) { + UpgradeTaskEntity task = loadTask(taskId); + UpgradeTaskEntity runningTask = upgradeTaskRepository.findRunningTask().orElse(null); + if (runningTask != null && !runningTask.getTaskId().equals(taskId)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "another upgrade task is running"); + } + if (activeExecutions.containsKey(taskId)) { + return toResponse(loadTask(taskId)); + } + if ("SUCCESS".equals(task.getStatus()) || "FAILED".equals(task.getStatus()) || "PENDING_CONFIRM".equals(task.getStatus())) { + upgradeTaskRepository.updateStatus(taskId, "RUNNING", "", task.getDetailLogPath(), LocalDateTime.now(), null); + } + + CompletableFuture future = CompletableFuture.runAsync(() -> { + try { + upgradeTaskRunner.run(loadTask(taskId)); + } finally { + activeExecutions.remove(taskId); + } + }, backgroundExecutor); + activeExecutions.put(taskId, future); + return toResponse(loadTask(taskId)); + } + + public UpgradeTaskPageResponse listTasks(int page, int pageSize, String targetComponent, String status) { + int normalizedPage = Math.max(1, page); + int normalizedPageSize = Math.max(1, pageSize); + int offset = (normalizedPage - 1) * normalizedPageSize; + List entities = upgradeTaskRepository.findPage(trim(targetComponent), trim(status), offset, normalizedPageSize); + long total = upgradeTaskRepository.countPage(trim(targetComponent), trim(status)); + + UpgradeTaskPageResponse response = new UpgradeTaskPageResponse(); + response.setPage(normalizedPage); + response.setPageSize(normalizedPageSize); + response.setTotal(total); + response.setItems(entities.stream().map(this::toResponse).collect(Collectors.toList())); + return response; + } + + public UpgradeTaskResponse getTask(String taskId) { + return toResponse(loadTask(taskId)); + } + + public UpgradeLogResponse getTaskLog(String taskId) { + UpgradeTaskEntity task = loadTask(taskId); + String logPath = trim(task.getDetailLogPath()); + if (logPath.isEmpty()) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade log not generated"); + } + Path path = Path.of(logPath).normalize().toAbsolutePath(); + Path allowedRoot = Path.of(trim(upgradeProperties.getLogDir())).normalize().toAbsolutePath(); + if (!path.startsWith(allowedRoot)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade log path is out of allowed directory"); + } + if (!Files.exists(path)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade log file does not exist"); + } + try { + UpgradeLogResponse response = new UpgradeLogResponse(); + response.setTaskId(taskId); + response.setLogPath(path.toString()); + response.setContent(Files.readString(path)); + return response; + } catch (IOException ex) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "failed to read upgrade log"); + } + } + + private UpgradeTaskEntity loadTask(String taskId) { + return upgradeTaskRepository.findByTaskId(trim(taskId)) + .orElseThrow(() -> new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade task not found: " + trim(taskId))); + } + + private UpgradeTaskResponse toResponse(UpgradeTaskEntity entity) { + UpgradeTaskResponse response = new UpgradeTaskResponse(); + response.setTaskId(entity.getTaskId()); + response.setUpgradeMode(entity.getUpgradeMode()); + response.setTargetComponent(entity.getTargetComponent()); + response.setPackageFileId(entity.getPackageFileId()); + response.setPackageName(entity.getPackageName()); + response.setCurrentVersion(entity.getCurrentVersion()); + response.setTargetVersion(entity.getTargetVersion()); + response.setStatus(entity.getStatus()); + response.setVerifyMessage(entity.getVerifyMessage()); + response.setErrorMessage(entity.getErrorMessage()); + response.setDetailLogPath(entity.getDetailLogPath()); + response.setStartedAt(entity.getStartedAt() == null ? "" : entity.getStartedAt().toString()); + response.setFinishedAt(entity.getFinishedAt() == null ? "" : entity.getFinishedAt().toString()); + return response; + } + + private String buildTaskId() { + return "UPG-" + LocalDateTime.now().format(TASK_ID_TS) + "-" + + UUID.randomUUID().toString().replace("-", "").substring(0, 6).toUpperCase(); + } + + private String trim(String value) { + return value == null ? "" : value.trim(); + } + + private String blankAs(String value, String fallback) { + return trim(value).isEmpty() ? fallback : trim(value); + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/support/NoopUpgradePackageSignatureVerifier.java b/src/main/java/com/cisd/tms/modules/upgrade/support/NoopUpgradePackageSignatureVerifier.java new file mode 100644 index 0000000..a304a40 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/support/NoopUpgradePackageSignatureVerifier.java @@ -0,0 +1,17 @@ +package com.cisd.tms.modules.upgrade.support; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.upgrade.dto.UpgradeManifest; +import java.nio.file.Files; +import java.nio.file.Path; + +public class NoopUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier { + + @Override + public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) { + if (!Files.exists(signaturePath)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found"); + } + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifier.java b/src/main/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifier.java new file mode 100644 index 0000000..fffa850 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifier.java @@ -0,0 +1,100 @@ +package com.cisd.tms.modules.upgrade.support; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds; +import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.upgrade.config.UpgradeProperties; +import com.cisd.tms.modules.upgrade.dto.UpgradeManifest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import org.springframework.stereotype.Component; + +@Component +public class PcieUpgradePackageSignatureVerifier implements UpgradePackageSignatureVerifier { + + private final UpgradeProperties upgradeProperties; + private final PcieCryptoService pcieCryptoService; + + public PcieUpgradePackageSignatureVerifier(UpgradeProperties upgradeProperties, PcieCryptoService pcieCryptoService) { + this.upgradeProperties = upgradeProperties; + this.pcieCryptoService = pcieCryptoService; + } + + @Override + public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) { + if (!Files.exists(signaturePath)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file not found"); + } + + byte[] publicKeyBlob = decodeConfiguredPublicKey(); + byte[] signature = loadSignature(signaturePath); + byte[] manifestBytes = readBytes(manifestPath, "failed to read manifest for signature verification"); + + EccExternalVerifyRequest request = new EccExternalVerifyRequest(); + request.setAlgId(Gm0018AlgorithmIds.SM2_SIGN); + request.setPublicKeyBlob(publicKeyBlob); + request.setData(manifestBytes); + request.setSignature(signature); + + try { + pcieCryptoService.eccExternalVerify(request); + } catch (BizException ex) { + throw ex; + } catch (RuntimeException ex) { + throw new BizException(ErrorCode.CRYPTO_CARD_ERROR.getCode(), "upgrade package signature verification failed"); + } + } + + private byte[] decodeConfiguredPublicKey() { + String configured = normalize(upgradeProperties.getSignaturePublicKeyBase64()); + if (configured.isEmpty()) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key not configured"); + } + try { + byte[] decoded = Base64.getDecoder().decode(configured); + if (decoded.length == 0) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key is invalid"); + } + return decoded; + } catch (IllegalArgumentException ex) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade signature public key is invalid"); + } + } + + private byte[] loadSignature(Path signaturePath) { + byte[] raw = readBytes(signaturePath, "failed to read upgrade signature file"); + if (raw.length == 0) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "upgrade package signature file is empty"); + } + + String text = new String(raw, StandardCharsets.UTF_8).trim(); + if (!text.isEmpty()) { + try { + byte[] decoded = Base64.getDecoder().decode(text); + if (decoded.length > 0) { + return decoded; + } + } catch (IllegalArgumentException ignored) { + // Keep backward compatibility with raw binary signature files. + } + } + return raw; + } + + private byte[] readBytes(Path path, String failureMessage) { + try { + return Files.readAllBytes(path); + } catch (IOException ex) { + throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), failureMessage); + } + } + + private String normalize(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageSignatureVerifier.java b/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageSignatureVerifier.java new file mode 100644 index 0000000..8878ad4 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageSignatureVerifier.java @@ -0,0 +1,9 @@ +package com.cisd.tms.modules.upgrade.support; + +import com.cisd.tms.modules.upgrade.dto.UpgradeManifest; +import java.nio.file.Path; + +public interface UpgradePackageSignatureVerifier { + + void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath); +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageStagingService.java b/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageStagingService.java new file mode 100644 index 0000000..1893515 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradePackageStagingService.java @@ -0,0 +1,58 @@ +package com.cisd.tms.modules.upgrade.support; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.upgrade.config.UpgradeProperties; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.springframework.stereotype.Service; + +@Service +public class UpgradePackageStagingService { + + private final UpgradeProperties upgradeProperties; + + public UpgradePackageStagingService(UpgradeProperties upgradeProperties) { + this.upgradeProperties = upgradeProperties; + } + + public Path stage(String fileId, Path packagePath) { + Path root = Paths.get(trim(upgradeProperties.getStagingRootDir())).normalize().toAbsolutePath(); + Path taskDir = root.resolve(fileId).normalize().toAbsolutePath(); + try { + Files.createDirectories(taskDir); + unzip(packagePath, taskDir); + return taskDir; + } catch (IOException ex) { + throw new BizException(ErrorCode.INTERNAL_ERROR.getCode(), "failed to stage upgrade package"); + } + } + + private void unzip(Path zipFile, Path targetDir) throws IOException { + try (InputStream inputStream = Files.newInputStream(zipFile); + ZipInputStream zipInputStream = new ZipInputStream(inputStream)) { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + Path target = targetDir.resolve(entry.getName()).normalize().toAbsolutePath(); + if (!target.startsWith(targetDir)) { + throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "invalid upgrade package entry"); + } + if (entry.isDirectory()) { + Files.createDirectories(target); + continue; + } + Files.createDirectories(target.getParent()); + Files.copy(zipInputStream, target, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + } + } + + private String trim(String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradeVersionComparator.java b/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradeVersionComparator.java new file mode 100644 index 0000000..b7ca7c7 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/upgrade/support/UpgradeVersionComparator.java @@ -0,0 +1,44 @@ +package com.cisd.tms.modules.upgrade.support; + +import java.util.ArrayList; +import java.util.List; +import org.springframework.stereotype.Component; + +@Component +public class UpgradeVersionComparator { + + public int compare(String left, String right) { + List leftParts = normalize(left); + List rightParts = normalize(right); + int size = Math.max(leftParts.size(), rightParts.size()); + for (int i = 0; i < size; i++) { + int lv = i < leftParts.size() ? leftParts.get(i) : 0; + int rv = i < rightParts.size() ? rightParts.get(i) : 0; + if (lv != rv) { + return Integer.compare(lv, rv); + } + } + return 0; + } + + private List normalize(String version) { + String value = version == null ? "" : version.trim(); + List parts = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + if (Character.isDigit(ch)) { + current.append(ch); + continue; + } + if (current.length() > 0) { + parts.add(Integer.parseInt(current.toString())); + current.setLength(0); + } + } + if (current.length() > 0) { + parts.add(Integer.parseInt(current.toString())); + } + return parts; + } +} diff --git a/src/main/java/com/cisd/tms/security/internal/InternalApiAuthInterceptor.java b/src/main/java/com/cisd/tms/security/internal/InternalApiAuthInterceptor.java index 594cc4b..0726bad 100644 --- a/src/main/java/com/cisd/tms/security/internal/InternalApiAuthInterceptor.java +++ b/src/main/java/com/cisd/tms/security/internal/InternalApiAuthInterceptor.java @@ -4,39 +4,109 @@ import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.common.config.properties.TmsSecurityProperties; import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.util.HttpResponseUtil; +import com.cisd.tms.modules.auth.entity.AuthSessionEntity; +import com.cisd.tms.modules.auth.repository.AuthSessionRepository; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Objects; +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.web.cors.CorsUtils; import org.springframework.web.servlet.HandlerInterceptor; @Component public class InternalApiAuthInterceptor implements HandlerInterceptor { - private static final String TOKEN_HEADER = "X-Internal-Token"; + public static final String ATTR_ROLE_CODE = "CURRENT_ROLE_CODE"; + public static final String ATTR_AUTH_LEVEL = "CURRENT_AUTH_LEVEL"; + public static final String ATTR_SESSION_TOKEN = "CURRENT_SESSION_TOKEN"; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String SESSION_HEADER = "X-Session-Token"; + private static final int IDLE_TIMEOUT_MINUTES = 10; + private final AuthSessionRepository authSessionRepository; private final TmsSecurityProperties securityProperties; private final ObjectMapper objectMapper; + private final Clock clock; - public InternalApiAuthInterceptor(TmsSecurityProperties securityProperties, ObjectMapper objectMapper) { + @Autowired + public InternalApiAuthInterceptor( + AuthSessionRepository authSessionRepository, + ObjectMapper objectMapper, + TmsSecurityProperties securityProperties + ) { + this(authSessionRepository, objectMapper, securityProperties, Clock.systemUTC()); + } + + InternalApiAuthInterceptor( + AuthSessionRepository authSessionRepository, + ObjectMapper objectMapper, + TmsSecurityProperties securityProperties, + Clock clock + ) { + this.authSessionRepository = authSessionRepository; this.securityProperties = securityProperties; this.objectMapper = objectMapper; + this.clock = clock; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - String expectedToken = securityProperties.getInternalToken(); - String actualToken = request.getHeader(TOKEN_HEADER); + if (CorsUtils.isPreFlightRequest(request)) { + return true; + } - return true; -// if (Objects.equals(expectedToken, actualToken)) { -// return true; -// } -// -// writeUnauthorized(response, "internal token invalid"); -// return false; + if (!securityProperties.getInternalAuth().isEnabled()) { + request.setAttribute(ATTR_SESSION_TOKEN, securityProperties.getInternalAuth().getDebugSessionToken()); + request.setAttribute(ATTR_ROLE_CODE, securityProperties.getInternalAuth().getDebugRoleCode()); + request.setAttribute(ATTR_AUTH_LEVEL, securityProperties.getInternalAuth().getDebugAuthLevel()); + return true; + } + + String sessionToken = extractSessionToken(request); + if (sessionToken == null || sessionToken.isBlank()) { + writeUnauthorized(response, "session token missing"); + return false; + } + + AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken).orElse(null); + if (session == null || session.getLogoutAt() != null) { + writeUnauthorized(response, "session not found"); + return false; + } + if (session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) { + writeUnauthorized(response, "session expired"); + return false; + } + + session.setLastActiveAt(now()); + session.setExpiresAt(now().plusMinutes(IDLE_TIMEOUT_MINUTES)); + authSessionRepository.update(session); + + request.setAttribute(ATTR_SESSION_TOKEN, sessionToken); + request.setAttribute(ATTR_ROLE_CODE, session.getRoleCode()); + request.setAttribute(ATTR_AUTH_LEVEL, session.getAuthLevel()); + return true; + } + + private String extractSessionToken(HttpServletRequest request) { + String authorization = request.getHeader(AUTHORIZATION_HEADER); + if (authorization != null && authorization.startsWith("Bearer ")) { + return authorization.substring("Bearer ".length()).trim(); + } + String sessionHeader = request.getHeader(SESSION_HEADER); + if (sessionHeader != null && !sessionHeader.isBlank()) { + return sessionHeader.trim(); + } + return null; + } + + private LocalDateTime now() { + return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC); } private void writeUnauthorized(HttpServletResponse response, String message) throws IOException { diff --git a/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java b/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java index 927bb3f..a0cca50 100644 --- a/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java +++ b/src/main/java/com/cisd/tms/security/openapi/OpenApiSignAuthInterceptor.java @@ -14,6 +14,7 @@ import java.security.MessageDigest; import java.time.Instant; import java.util.Map; import org.springframework.stereotype.Component; +import org.springframework.web.cors.CorsUtils; import org.springframework.web.servlet.HandlerInterceptor; @Component @@ -40,6 +41,10 @@ public class OpenApiSignAuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (CorsUtils.isPreFlightRequest(request)) { + return true; + } + String appId = trimHeader(request, APP_ID_HEADER); String timestamp = trimHeader(request, TIMESTAMP_HEADER); String nonce = trimHeader(request, NONCE_HEADER); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c8b35b6..e421610 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -167,9 +167,64 @@ tms: memory-total-default: ${TMS_DEVICE_PROFILE_MEMORY_TOTAL_DEFAULT:16GB} # 磁盘总量默认展示值;命令读取失败或为空时回退。 disk-total-default: ${TMS_DEVICE_PROFILE_DISK_TOTAL_DEFAULT:256G} + web: + cors: + # 是否启用浏览器跨域访问控制。 + enabled: ${TMS_WEB_CORS_ENABLED:true} + # 允许的前端来源;默认放开本机联调端口,部署时请改成明确前端域名/IP。 + allowed-origin-patterns: + - ${TMS_WEB_CORS_ALLOWED_ORIGIN_PATTERN_1:http://localhost:*} + - ${TMS_WEB_CORS_ALLOWED_ORIGIN_PATTERN_2:http://127.0.0.1:*} + # 允许浏览器预检和业务请求使用的方法。 + allowed-methods: + - GET + - POST + - PUT + - DELETE + - OPTIONS + # 允许前端发送的请求头;覆盖内部 token、OpenAPI 签名头和 traceId。 + allowed-headers: + - Authorization + - Content-Type + - X-Requested-With + - X-Internal-Token + - X-App-Id + - X-Timestamp + - X-Nonce + - X-Signature + - X-Trace-Id + # 允许前端读取的响应头。 + exposed-headers: + - X-Trace-Id + # 是否允许跨域携带 cookie / 凭据。 + allow-credentials: ${TMS_WEB_CORS_ALLOW_CREDENTIALS:true} + # 浏览器预检缓存时间(秒)。 + max-age-seconds: ${TMS_WEB_CORS_MAX_AGE_SECONDS:3600} + upgrade: + # 升级包解压和脚本执行暂存目录。 + staging-root-dir: ${TMS_UPGRADE_STAGING_ROOT_DIR:/home/tmp/tms-upgrade-staging} + # 升级任务统一日志目录。 + log-dir: ${TMS_UPGRADE_LOG_DIR:/home/tms/tmp/tms-upgrade-logs} + # 升级包验签使用的“固件完整性密钥”公钥 blob(Base64 编码);为空时升级预检会拒绝通过。 + signature-public-key-base64: ${TMS_UPGRADE_SIGNATURE_PUBLIC_KEY_BASE64:} + mk: + init-identify: + # SDFE_InitIdentify 旧 PIN(Base64 编码);主密钥初始化接口会读取该值。 + old-pin-base64: ${TMS_MK_INIT_IDENTIFY_OLD_PIN_BASE64:NjY2NjY2} + # SDFE_InitIdentify 新 PIN(Base64 编码);主密钥初始化接口会读取该值。 + new-pin-base64: ${TMS_MK_INIT_IDENTIFY_NEW_PIN_BASE64:NjY2NjY2} security: # 内部接口鉴权 token(前后端内部调用需一致)。 internal-token: ${TMS_INTERNAL_TOKEN:change-me-internal-token} + internal-auth: + # 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。 + enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:false} + # 关闭内部鉴权后,注入请求上下文的调试角色。 + debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:KEY_ADMIN} + # 关闭内部鉴权后,注入请求上下文的调试认证等级。 + debug-auth-level: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_AUTH_LEVEL:FULL} + # 关闭内部鉴权后,注入请求上下文的调试 session token。 + debug-session-token: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_SESSION_TOKEN:DEBUG-BYPASS} openapi: # 外部签名服务接口允许的时间戳偏差(秒),防重放。 timestamp-skew-seconds: 300 @@ -182,8 +237,8 @@ tms: # 密码卡模式:MOCK(模拟)/JNA(动态库调用)等。 mode: ${TMS_CRYPTO_CARD_MODE:JNA} # 动态库目录(.so 所在目录)。 - vendor-lib-path: ${TMS_CRYPTO_VENDOR_LIB_PATH:/home/tms/lib/libsdf_syd1408-x64.so.1.0.0} + vendor-lib-path: ${TMS_CRYPTO_VENDOR_LIB_PATH:/home/tms/libs/libsdf_ukey_syd1408-x64.so.1.0.0} # 动态库名(不含前缀/后缀或按加载器约定)。 - vendor-lib-name: ${TMS_CRYPTO_VENDOR_LIB_NAME:syd1408} + vendor-lib-name: ${TMS_CRYPTO_VENDOR_LIB_NAME:sdf_ukey_syd1408} # 是否严格校验算法标识(与 0018/厂商补充定义一致性校验)。 strict-alg-id-validation: ${TMS_CRYPTO_STRICT_ALG_ID_VALIDATION:true} diff --git a/src/main/resources/db/migration/V10__create_t_upgrade_task.sql b/src/main/resources/db/migration/V10__create_t_upgrade_task.sql new file mode 100644 index 0000000..e9adbad --- /dev/null +++ b/src/main/resources/db/migration/V10__create_t_upgrade_task.sql @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS t_upgrade_task ( + id BIGINT PRIMARY KEY, + task_id VARCHAR(64) NOT NULL, + upgrade_mode VARCHAR(16) NOT NULL, + target_component VARCHAR(32) NOT NULL, + package_file_id VARCHAR(64) NOT NULL, + package_name VARCHAR(255) NOT NULL, + package_hash VARCHAR(128) NOT NULL, + current_version VARCHAR(64) NOT NULL, + target_version VARCHAR(64) NOT NULL, + status VARCHAR(32) NOT NULL, + verify_passed TINYINT(1) NOT NULL DEFAULT 0, + verify_message VARCHAR(1024) NULL, + manifest_json TEXT NULL, + detail_log_path VARCHAR(1024) NULL, + error_message VARCHAR(1024) NULL, + operator VARCHAR(64) NULL, + started_at DATETIME(3) NULL, + finished_at DATETIME(3) NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_t_upgrade_task_task_id (task_id), + KEY idx_t_upgrade_task_status (status), + KEY idx_t_upgrade_task_component (target_component), + KEY idx_t_upgrade_task_create_time (create_time) +); diff --git a/src/main/resources/db/migration/V11__auth_role_ukey_session_tables.sql b/src/main/resources/db/migration/V11__auth_role_ukey_session_tables.sql new file mode 100644 index 0000000..97fd5ee --- /dev/null +++ b/src/main/resources/db/migration/V11__auth_role_ukey_session_tables.sql @@ -0,0 +1,157 @@ +CREATE TABLE IF NOT EXISTS tms_role_account ( + id BIGINT PRIMARY KEY, + role_code VARCHAR(64) NOT NULL, + display_name VARCHAR(128) NOT NULL, + required_ukey_count INT NOT NULL, + password_hash VARCHAR(256) NOT NULL, + password_salt VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL, + need_change_password TINYINT(1) NOT NULL DEFAULT 1, + failed_count INT NOT NULL DEFAULT 0, + locked_until DATETIME(3) NULL, + last_login_at DATETIME(3) NULL, + last_active_at DATETIME(3) NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_tms_role_account_role_code (role_code) +); + +CREATE TABLE IF NOT EXISTS tms_role_ukey_binding ( + id BIGINT PRIMARY KEY, + role_code VARCHAR(64) NOT NULL, + slot_no INT NOT NULL, + ukey_serial VARCHAR(128) NOT NULL, + ukey_pubkey TEXT NOT NULL, + issuer_sign TEXT NULL, + status VARCHAR(32) NOT NULL, + bound_at DATETIME(3) NOT NULL, + unbound_at DATETIME(3) NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_tms_role_ukey_binding_role_slot (role_code, slot_no), + KEY idx_tms_role_ukey_binding_role_status (role_code, status) +); + +CREATE TABLE IF NOT EXISTS tms_auth_session ( + id BIGINT PRIMARY KEY, + session_token VARCHAR(128) NOT NULL, + role_code VARCHAR(64) NOT NULL, + auth_level VARCHAR(32) NOT NULL, + issued_at DATETIME(3) NOT NULL, + last_active_at DATETIME(3) NOT NULL, + expires_at DATETIME(3) NOT NULL, + logout_at DATETIME(3) NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_tms_auth_session_token (session_token), + KEY idx_tms_auth_session_role_code (role_code) +); + +CREATE TABLE IF NOT EXISTS tms_auth_challenge ( + id BIGINT PRIMARY KEY, + challenge_id VARCHAR(128) NOT NULL, + role_code VARCHAR(64) NOT NULL, + purpose VARCHAR(32) NOT NULL, + nonce VARCHAR(256) NOT NULL, + expires_at DATETIME(3) NOT NULL, + used TINYINT(1) NOT NULL DEFAULT 0, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + UNIQUE KEY uk_tms_auth_challenge_challenge_id (challenge_id) +); + +CREATE TABLE IF NOT EXISTS tms_auth_audit_log ( + id BIGINT PRIMARY KEY, + event_type VARCHAR(64) NOT NULL, + role_code VARCHAR(64) NULL, + operator_role_code VARCHAR(64) NULL, + result VARCHAR(32) NOT NULL, + detail_json JSON NULL, + remote_ip VARCHAR(64) NULL, + create_time DATETIME(3) NOT NULL, + update_time DATETIME(3) NOT NULL, + KEY idx_tms_auth_audit_log_event_type (event_type), + KEY idx_tms_auth_audit_log_role_code (role_code) +); + +INSERT IGNORE INTO tms_role_account ( + id, + role_code, + display_name, + required_ukey_count, + password_hash, + password_salt, + status, + need_change_password, + failed_count, + locked_until, + last_login_at, + last_active_at, + create_time, + update_time +) +VALUES + ( + 11001, + 'SUPER_ADMIN', + '超级管理员', + 3, + 'Viuew8WE+qziUZwGGU0x25cUpzsTF+QvQFU0uhb+yVA=', + 'init-super-admin-salt-20260325', + 'UNENABLED', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ), + ( + 11002, + 'KEY_ADMIN', + '密钥管理员', + 2, + '/xqmDy3A/q9X5p7XWznIPSBabQ6bxTLAPgtHQ6Be33c=', + 'init-key-admin-salt-20260325', + 'ACTIVE', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ), + ( + 11003, + 'AUDIT_ADMIN', + '审计管理员', + 1, + 'j/J1LKyUOzVEJfbbHe2dwKBKNdabRk502olk/LWIwhg=', + 'init-audit-admin-salt-20260325', + 'UNENABLED', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ), + ( + 11004, + 'OPS_ADMIN', + '运维管理员', + 1, + '2e9F4aCgsDb+AuD1LrBEqEg8yjy3ODo27UHtx06vj/A=', + 'init-ops-admin-salt-20260325', + 'UNENABLED', + 1, + 0, + NULL, + NULL, + NULL, + CURRENT_TIMESTAMP(3), + CURRENT_TIMESTAMP(3) + ); diff --git a/src/main/resources/db/migration/V6__extend_init_task_type.sql b/src/main/resources/db/migration/V6__extend_init_task_type.sql new file mode 100644 index 0000000..3cdc255 --- /dev/null +++ b/src/main/resources/db/migration/V6__extend_init_task_type.sql @@ -0,0 +1,2 @@ +ALTER TABLE tms_init_task + ADD COLUMN task_type VARCHAR(32) NOT NULL DEFAULT 'INIT' COMMENT '任务类型:INIT/RESET' AFTER task_id; diff --git a/src/main/resources/db/migration/V7__create_sys_device_software_version.sql b/src/main/resources/db/migration/V7__create_sys_device_software_version.sql new file mode 100644 index 0000000..3cb7de2 --- /dev/null +++ b/src/main/resources/db/migration/V7__create_sys_device_software_version.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS sys_device_software_version ( + id BIGINT PRIMARY KEY, + component_code VARCHAR(64) NOT NULL, + component_name VARCHAR(128) NOT NULL, + current_version VARCHAR(128) NOT NULL, + source_type VARCHAR(32) NOT NULL, + detected_at DATETIME NULL, + remarks VARCHAR(255) NULL, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uk_sys_device_software_version_component_code (component_code) +); diff --git a/src/main/resources/db/migration/V8__rename_sys_device_software_version_table.sql b/src/main/resources/db/migration/V8__rename_sys_device_software_version_table.sql new file mode 100644 index 0000000..3051105 --- /dev/null +++ b/src/main/resources/db/migration/V8__rename_sys_device_software_version_table.sql @@ -0,0 +1 @@ +RENAME TABLE sys_device_software_version TO tms_device_software_version; diff --git a/src/main/resources/db/migration/V9__seed_tms_device_software_version.sql b/src/main/resources/db/migration/V9__seed_tms_device_software_version.sql new file mode 100644 index 0000000..f7327ff --- /dev/null +++ b/src/main/resources/db/migration/V9__seed_tms_device_software_version.sql @@ -0,0 +1,15 @@ +INSERT INTO tms_device_software_version +(id, component_code, component_name, current_version, source_type, detected_at, remarks) +VALUES + (1001, 'RABBITMQ', '消息队列', 'RabbitMQ V3.8.21', 'MANUAL', NOW(), '出厂预置'), + (1002, 'TLQ', '消息队列', 'tonglink/Q V8.1.11', 'MANUAL', NOW(), '出厂预置'), + (1003, 'TIDB', '数据库', 'TiDB V8.1.0', 'MANUAL', NOW(), '出厂预置'), + (1004, 'NGINX', 'Nginx', 'Nginx V1.20', 'MANUAL', NOW(), '出厂预置'), + (1005, 'APP', '标准收发器企业版', '标准收发器企业版 V1.3.2', 'MANUAL', NOW(), '出厂预置') +ON DUPLICATE KEY UPDATE + component_name = VALUES(component_name), + current_version = VALUES(current_version), + source_type = VALUES(source_type), + detected_at = VALUES(detected_at), + remarks = VALUES(remarks), + update_time = CURRENT_TIMESTAMP; diff --git a/src/main/resources/mapper/auth/AuthSessionMapper.xml b/src/main/resources/mapper/auth/AuthSessionMapper.xml new file mode 100644 index 0000000..ba40e1e --- /dev/null +++ b/src/main/resources/mapper/auth/AuthSessionMapper.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/auth/RoleAccountMapper.xml b/src/main/resources/mapper/auth/RoleAccountMapper.xml new file mode 100644 index 0000000..7616ab6 --- /dev/null +++ b/src/main/resources/mapper/auth/RoleAccountMapper.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml b/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml new file mode 100644 index 0000000..f1069ad --- /dev/null +++ b/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/device/DeviceSoftwareVersionMapper.xml b/src/main/resources/mapper/device/DeviceSoftwareVersionMapper.xml new file mode 100644 index 0000000..2ed322b --- /dev/null +++ b/src/main/resources/mapper/device/DeviceSoftwareVersionMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + UPDATE tms_device_software_version + SET component_name = #{componentName}, + current_version = #{currentVersion}, + source_type = #{sourceType}, + detected_at = #{detectedAt}, + remarks = #{remarks}, + update_time = NOW() + WHERE component_code = #{componentCode} + + + diff --git a/src/main/resources/mapper/upgrade/UpgradeTaskMapper.xml b/src/main/resources/mapper/upgrade/UpgradeTaskMapper.xml new file mode 100644 index 0000000..bf541d1 --- /dev/null +++ b/src/main/resources/mapper/upgrade/UpgradeTaskMapper.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id, task_id, upgrade_mode, target_component, package_file_id, package_name, package_hash, + current_version, target_version, status, verify_passed, verify_message, manifest_json, + detail_log_path, error_message, operator, started_at, finished_at, create_time, update_time + + + + + + + + + + + + UPDATE t_upgrade_task + SET status = #{status}, + error_message = #{errorMessage}, + detail_log_path = #{detailLogPath}, + started_at = #{startedAt}, + finished_at = #{finishedAt}, + update_time = NOW() + WHERE task_id = #{taskId} + + + diff --git a/src/test/java/com/cisd/tms/TmsApplicationTests.java b/src/test/java/com/cisd/tms/TmsApplicationTests.java index a0ec30f..babf7c1 100644 --- a/src/test/java/com/cisd/tms/TmsApplicationTests.java +++ b/src/test/java/com/cisd/tms/TmsApplicationTests.java @@ -1,9 +1,11 @@ package com.cisd.tms; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.core.io.ClassPathResource; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -37,4 +39,24 @@ class TmsApplicationTests { .andExpect(content().string(org.hamcrest.Matchers.containsString("\"/api/v1/auth/login\""))) .andExpect(content().string(org.hamcrest.Matchers.containsString("\"summary\":\"内部用户登录\""))); } + + @Test + void shouldCreateAuthRoleTablesAndSeedFixedRoleAccounts() { + ClassPathResource migration = new ClassPathResource("db/migration/V11__auth_role_ukey_session_tables.sql"); + + org.junit.jupiter.api.Assertions.assertTrue(migration.exists()); + + String sql = org.junit.jupiter.api.Assertions.assertDoesNotThrow( + () -> new String(migration.getInputStream().readAllBytes(), StandardCharsets.UTF_8) + ); + + org.junit.jupiter.api.Assertions.assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS tms_role_account")); + org.junit.jupiter.api.Assertions.assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS tms_role_ukey_binding")); + org.junit.jupiter.api.Assertions.assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS tms_auth_session")); + org.junit.jupiter.api.Assertions.assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS tms_auth_challenge")); + org.junit.jupiter.api.Assertions.assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS tms_auth_audit_log")); + org.junit.jupiter.api.Assertions.assertTrue(sql.contains("'KEY_ADMIN'")); + org.junit.jupiter.api.Assertions.assertTrue(sql.contains("'ACTIVE'")); + org.junit.jupiter.api.Assertions.assertTrue(sql.contains("'UNENABLED'")); + } } diff --git a/src/test/java/com/cisd/tms/integration/crypto/pcie/PcieNativeMappingContractTest.java b/src/test/java/com/cisd/tms/integration/crypto/pcie/PcieNativeMappingContractTest.java new file mode 100644 index 0000000..429141c --- /dev/null +++ b/src/test/java/com/cisd/tms/integration/crypto/pcie/PcieNativeMappingContractTest.java @@ -0,0 +1,51 @@ +package com.cisd.tms.integration.crypto.pcie; + +import com.cisd.tms.integration.crypto.pcie.jna.PcieNativeLibrary; +import com.cisd.tms.integration.crypto.pcie.jna.SdfeDeviceConf; +import com.cisd.tms.integration.crypto.pcie.jna.SdfeLoginState; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import java.lang.reflect.Method; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class PcieNativeMappingContractTest { + + @Test + void shouldIncludeTypeFieldInSdfeDeviceConfLayout() throws Exception { + Assertions.assertNotNull(SdfeDeviceConf.class.getDeclaredField("type")); + Assertions.assertEquals(32, new SdfeDeviceConf().size()); + } + + @Test + void shouldMapDeviceStatusGetToStructuredStatusType() throws Exception { + Class deviceStatusClass = Class.forName( + "com.cisd.tms.integration.crypto.pcie.jna.SdfeDeviceStatus" + ); + + Method method = PcieNativeLibrary.class.getMethod( + "SDFE_DeviceStatusGet", + Pointer.class, + deviceStatusClass + ); + + Assertions.assertNotNull(method); + Assertions.assertEquals(76, ((Structure) deviceStatusClass.getDeclaredConstructor().newInstance()).size()); + } + + @Test + void shouldExposeSdkUkeySymbolNames() throws Exception { + Assertions.assertNotNull( + PcieNativeLibrary.class.getMethod("SDFE_ReadUkeyLMKComponent", Pointer.class, byte[].class, int.class) + ); + Assertions.assertNotNull( + PcieNativeLibrary.class.getMethod("SDFE_GenerateUkeyCertificate", Pointer.class, byte[].class, int.class) + ); + Assertions.assertNotNull( + PcieNativeLibrary.class.getMethod("SDFE_LoginoutUkey", Pointer.class, byte.class) + ); + Assertions.assertNotNull( + PcieNativeLibrary.class.getMethod("SDFE_UkeyLoginStatus", Pointer.class, SdfeLoginState.class) + ); + } +} diff --git a/src/test/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplateTest.java b/src/test/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplateTest.java new file mode 100644 index 0000000..ed68112 --- /dev/null +++ b/src/test/java/com/cisd/tms/integration/crypto/pcie/PcieSessionTemplateTest.java @@ -0,0 +1,100 @@ +package com.cisd.tms.integration.crypto.pcie; + +import com.cisd.tms.integration.crypto.config.CryptoCardProperties; +import com.cisd.tms.integration.crypto.pcie.jna.PcieNativeLibrary; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.PointerByReference; +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class PcieSessionTemplateTest { + + @Test + void shouldReuseDeviceHandleAcrossMultipleSessionsAndCloseOnShutdown() throws Exception { + PcieSessionTemplate template = new PcieSessionTemplate(new CryptoCardProperties(), new PcieErrorMapper()); + PcieNativeLibrary lib = Mockito.mock(PcieNativeLibrary.class); + AtomicInteger sessionSeed = new AtomicInteger(10); + + Mockito.when(lib.SDF_OpenDevice(Mockito.any())).thenAnswer(invocation -> { + PointerByReference ref = invocation.getArgument(0); + ref.setValue(Pointer.createConstant(1)); + return 0; + }); + Mockito.when(lib.SDF_OpenSession(Mockito.any(), Mockito.any())).thenAnswer(invocation -> { + PointerByReference ref = invocation.getArgument(1); + ref.setValue(Pointer.createConstant(sessionSeed.incrementAndGet())); + return 0; + }); + Mockito.when(lib.SDF_CloseSession(Mockito.any())).thenReturn(0); + Mockito.when(lib.SDF_CloseDevice(Mockito.any())).thenReturn(0); + + setField(template, "nativeLibrary", lib); + + String first = template.withSession("first", (nativeLib, deviceHandle, sessionHandle) -> { + Assertions.assertEquals(Pointer.createConstant(1), deviceHandle); + Assertions.assertEquals(Pointer.createConstant(11), sessionHandle); + return "first-ok"; + }); + String second = template.withSession("second", (nativeLib, deviceHandle, sessionHandle) -> { + Assertions.assertEquals(Pointer.createConstant(1), deviceHandle); + Assertions.assertEquals(Pointer.createConstant(12), sessionHandle); + return "second-ok"; + }); + + Assertions.assertEquals("first-ok", first); + Assertions.assertEquals("second-ok", second); + Mockito.verify(lib, Mockito.times(1)).SDF_OpenDevice(Mockito.any()); + Mockito.verify(lib, Mockito.times(2)).SDF_OpenSession(Mockito.any(), Mockito.any()); + Mockito.verify(lib, Mockito.times(2)).SDF_CloseSession(Mockito.any()); + Mockito.verify(lib, Mockito.never()).SDF_CloseDevice(Mockito.any()); + + template.closeSharedDevice(); + + Mockito.verify(lib, Mockito.times(1)).SDF_CloseDevice(Mockito.eq(Pointer.createConstant(1))); + } + + @Test + void shouldReopenSharedDeviceAndRetryOnceWhenOpenSessionFails() throws Exception { + PcieSessionTemplate template = new PcieSessionTemplate(new CryptoCardProperties(), new PcieErrorMapper()); + PcieNativeLibrary lib = Mockito.mock(PcieNativeLibrary.class); + AtomicInteger deviceSeed = new AtomicInteger(0); + + Mockito.when(lib.SDF_OpenDevice(Mockito.any())).thenAnswer(invocation -> { + PointerByReference ref = invocation.getArgument(0); + ref.setValue(Pointer.createConstant(deviceSeed.incrementAndGet())); + return 0; + }); + Mockito.when(lib.SDF_OpenSession(Mockito.any(), Mockito.any())) + .thenReturn(0x25) + .thenAnswer(invocation -> { + PointerByReference ref = invocation.getArgument(1); + ref.setValue(Pointer.createConstant(21)); + return 0; + }); + Mockito.when(lib.SDF_CloseSession(Mockito.any())).thenReturn(0); + Mockito.when(lib.SDF_CloseDevice(Mockito.any())).thenReturn(0); + + setField(template, "nativeLibrary", lib); + + String result = template.withSession("recover", (nativeLib, deviceHandle, sessionHandle) -> { + Assertions.assertEquals(Pointer.createConstant(2), deviceHandle); + Assertions.assertEquals(Pointer.createConstant(21), sessionHandle); + return "recovered"; + }); + + Assertions.assertEquals("recovered", result); + Mockito.verify(lib, Mockito.times(2)).SDF_OpenDevice(Mockito.any()); + Mockito.verify(lib, Mockito.times(2)).SDF_OpenSession(Mockito.any(), Mockito.any()); + Mockito.verify(lib, Mockito.times(1)).SDF_CloseDevice(Mockito.eq(Pointer.createConstant(1))); + Mockito.verify(lib, Mockito.times(1)).SDF_CloseSession(Mockito.eq(Pointer.createConstant(21))); + } + + private static void setField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/src/test/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoServiceTest.java b/src/test/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoServiceTest.java new file mode 100644 index 0000000..6a45036 --- /dev/null +++ b/src/test/java/com/cisd/tms/integration/crypto/pcie/service/JnaPcieCryptoServiceTest.java @@ -0,0 +1,50 @@ +package com.cisd.tms.integration.crypto.pcie.service; + +import com.cisd.tms.integration.crypto.config.CryptoCardProperties; +import com.cisd.tms.integration.crypto.pcie.PcieSessionTemplate; +import com.cisd.tms.integration.crypto.pcie.jna.PcieNativeLibrary; +import com.sun.jna.Pointer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class JnaPcieCryptoServiceTest { + + @Test + void shouldInitializeMasterKeyWithinSingleSession() { + PcieSessionTemplate sessionTemplate = Mockito.mock(PcieSessionTemplate.class); + CryptoCardProperties properties = new CryptoCardProperties(); + JnaPcieCryptoService service = new JnaPcieCryptoService(sessionTemplate, properties); + + Mockito.when(sessionTemplate.withSession(Mockito.eq("SDFE_InitializeMasterKey"), Mockito.any())) + .thenAnswer(invocation -> { + @SuppressWarnings("unchecked") + PcieSessionTemplate.SessionCallback callback = + (PcieSessionTemplate.SessionCallback) invocation.getArgument(1); + PcieNativeLibrary lib = Mockito.mock(PcieNativeLibrary.class); + byte[] seedMac = new byte[] {0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF}; + Mockito.when(lib.SDFE_InitIdentify(Mockito.any(), Mockito.any(), Mockito.anyInt(), Mockito.any(), Mockito.anyInt())).thenReturn(0); + Mockito.when(lib.SDFE_GenerateLMK(Mockito.any())).thenReturn(0); + Mockito.when(lib.SDFE_LoadLMK(Mockito.any())).thenReturn(0); + Mockito.when(lib.SDFE_GenerateIK(Mockito.any(), Mockito.eq(2))).thenReturn(0); + Mockito.when(lib.SDFE_GenerateIK(Mockito.any(), Mockito.eq(1))).thenReturn(0); + Mockito.when(lib.SDFE_CheckLMK(Mockito.any())).thenReturn(0); + Mockito.when(lib.SDFE_ExportLMKSeedMAC(Mockito.any(), Mockito.any())).thenAnswer(exportInvocation -> { + byte[] out = exportInvocation.getArgument(1); + System.arraycopy(seedMac, 0, out, 0, seedMac.length); + return 0; + }); + return callback.apply(lib, Pointer.createConstant(1), Pointer.createConstant(2)); + }); + + byte[] mac = service.initializeMasterKey( + "12345678".getBytes(StandardCharsets.UTF_8), + "87654321".getBytes(StandardCharsets.UTF_8) + ); + + Assertions.assertArrayEquals(new byte[] {0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF}, mac); + Mockito.verify(sessionTemplate).withSession(Mockito.eq("SDFE_InitializeMasterKey"), Mockito.any()); + } +} diff --git a/src/test/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceInfoFormatterTest.java b/src/test/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceInfoFormatterTest.java new file mode 100644 index 0000000..3619cf7 --- /dev/null +++ b/src/test/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceInfoFormatterTest.java @@ -0,0 +1,30 @@ +package com.cisd.tms.integration.crypto.pcie.service; + +import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class PcieDeviceInfoFormatterTest { + + @Test + void shouldFormatReadableDeviceInfoFieldsFromRawCapabilities() { + DeviceInfoResult result = new DeviceInfoResult(); + result.setDeviceVersion(808463921); + result.setStandardVersion(1581074); + result.setAsymAlgAbility0(200192); + result.setAsymAlgAbility1(134217984); + result.setSymAlgAbility(230656); + result.setHashAlgAbility(5); + result.setBufferSize(65536); + + PcieDeviceInfoFormatter.enrich(result); + + Assertions.assertEquals("V1.00", result.getDeviceVersionText()); + Assertions.assertEquals("GMT 0018-2012", result.getStandardVersionText()); + Assertions.assertEquals("SM2_SIGN SM2_DH SM2_ENC RSA", result.getAsymmetricAlgorithmsText()); + Assertions.assertEquals("256 2048 bits", result.getAsymmetricKeyBitsText()); + Assertions.assertEquals("SM1 SM4 AES128 AES192 AES256", result.getSymmetricAlgorithmsText()); + Assertions.assertEquals("SM3 SHA256", result.getHashAlgorithmsText()); + Assertions.assertEquals("64 KB", result.getBufferSizeText()); + } +} diff --git a/src/test/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceStatusFormatterTest.java b/src/test/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceStatusFormatterTest.java new file mode 100644 index 0000000..7fcad4d --- /dev/null +++ b/src/test/java/com/cisd/tms/integration/crypto/pcie/service/PcieDeviceStatusFormatterTest.java @@ -0,0 +1,51 @@ +package com.cisd.tms.integration.crypto.pcie.service; + +import com.cisd.tms.integration.crypto.pcie.jna.SdfeDeviceStatus; +import com.cisd.tms.integration.crypto.pcie.model.DeviceStatusResult; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class PcieDeviceStatusFormatterTest { + + @Test + void shouldFormatReadableDeviceStatusFields() { + SdfeDeviceStatus status = new SdfeDeviceStatus(); + status.fsmState = 0; + status.commit = 0x00018219; + status.frequency = 500; + status.pcieSpeed = 2; + status.pcieWidth = 8; + status.boardTemp = 44.44f; + status.boardVoltage = 12.18f; + status.fsTotalKb = 47; + status.fsFreeKb = 44; + + DeviceStatusResult result = new DeviceStatusResult(); + result.setLength(4); + result.setStatusData(new byte[]{0x01, 0x23, 0x45, 0x67}); + + PcieDeviceStatusFormatter.enrich(result, status); + + Assertions.assertEquals("01234567", result.getStatusHex()); + Assertions.assertEquals("就绪状态", result.getFsmStateText()); + Assertions.assertEquals("0x00018219", result.getCommitHex()); + Assertions.assertEquals("500M", result.getFrequencyText()); + Assertions.assertEquals("GEN2 X8", result.getPcieInfoText()); + Assertions.assertEquals("44.44 °C", result.getBoardTempText()); + Assertions.assertEquals("12.18 V", result.getBoardVoltageText()); + Assertions.assertEquals("47 KB", result.getFsTotalText()); + Assertions.assertEquals("44 KB", result.getFsFreeText()); + } + + @Test + void shouldFormatUninitializedState() { + SdfeDeviceStatus status = new SdfeDeviceStatus(); + status.fsmState = 1; + + DeviceStatusResult result = new DeviceStatusResult(); + + PcieDeviceStatusFormatter.enrich(result, status); + + Assertions.assertEquals("未初始化状态", result.getFsmStateText()); + } +} diff --git a/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java b/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java new file mode 100644 index 0000000..5eb9756 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java @@ -0,0 +1,193 @@ +package com.cisd.tms.modules.auth.controller; + +import com.cisd.tms.common.exception.GlobalExceptionHandler; +import com.cisd.tms.modules.auth.dto.CurrentUserResponse; +import com.cisd.tms.modules.auth.dto.LoginResponse; +import com.cisd.tms.modules.auth.service.AuthAdminService; +import com.cisd.tms.modules.auth.service.AuthService; +import com.cisd.tms.modules.mk.dto.UKeySignResult; +import com.cisd.tms.security.internal.InternalApiAuthInterceptor; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class AuthControllerTest { + + @Test + void shouldLoginWithRoleBasedPayload() throws Exception { + AuthService authService = Mockito.mock(AuthService.class); + AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); + LoginResponse response = new LoginResponse(); + response.setRoleCode("KEY_ADMIN"); + response.setAuthLevel("FULL"); + response.setToken("token-001"); + response.setExpiresAt("2026-03-23T03:10Z"); + response.setNeedChangePassword(false); + Mockito.when(authService.login(ArgumentMatchers.any())).thenReturn(response); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"roleCode\":\"KEY_ADMIN\",\"password\":\"12345678\",\"ukeySerials\":[\"UK-1\",\"UK-2\"]}")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"roleCode\":\"KEY_ADMIN\""))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"authLevel\":\"FULL\""))); + } + + @Test + void shouldEnableRoleThroughAdminEndpoint() throws Exception { + AuthService authService = Mockito.mock(AuthService.class); + AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/auth/roles/AUDIT_ADMIN/enable") + .requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN") + .requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"success\":true"))); + + Mockito.verify(authAdminService).enableRole("KEY_ADMIN", "FULL", "AUDIT_ADMIN"); + } + + @Test + void shouldBindUkeyThroughAdminEndpoint() throws Exception { + AuthService authService = Mockito.mock(AuthService.class); + AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/auth/roles/SUPER_ADMIN/ukeys/bind") + .requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN") + .requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "slotNo": 1, + "ukeySerial": "UK-NEW", + "ukeyPubkey": "PUB-NEW", + "uid": "1", + "rid": "RID-001", + "issuerSign": "SIG-NEW" + } + """)) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"success\":true"))); + + Mockito.verify(authAdminService).bindIssuedUkey("KEY_ADMIN", "FULL", "SUPER_ADMIN", 1, "UK-NEW", "PUB-NEW", "1", "RID-001", "SIG-NEW"); + } + + @Test + void shouldIssueUkeyBindingSignThroughAdminEndpoint() throws Exception { + AuthService authService = Mockito.mock(AuthService.class); + AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); + UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-001").component("COMP-001").build(); + Mockito.when(authAdminService.issueUkeyBindingSign( + ArgumentMatchers.eq("KEY_ADMIN"), + ArgumentMatchers.eq("FULL"), + ArgumentMatchers.eq("SUPER_ADMIN"), + ArgumentMatchers.any() + )).thenReturn(response); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/auth/roles/SUPER_ADMIN/ukeys/issue-sign") + .requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN") + .requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "pubKey": "PUB-NEW", + "role": "superadmin", + "uid": "1", + "rid": "RID-001", + "extra": "EXTRA-001" + } + """)) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"sign\":\"ISSUE-SIGN-001\""))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"extra\":\"COMP-001\""))); + } + + @Test + void shouldReadCurrentRoleFromRequestContextForMeEndpoint() throws Exception { + AuthService authService = Mockito.mock(AuthService.class); + AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); + CurrentUserResponse response = new CurrentUserResponse(); + response.setUsername("AUDIT_ADMIN"); + response.setDisplayName("审计管理员"); + response.setRole("AUDIT_ADMIN"); + response.setAuthLevel("LIMITED"); + response.setNeedChangePassword(false); + Mockito.when(authService.me("AUDIT_ADMIN", "token-me-001")).thenReturn(response); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(get("/api/v1/auth/me") + .requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-me-001") + .requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"role\":\"AUDIT_ADMIN\""))) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"authLevel\":\"LIMITED\""))); + } + + @Test + void shouldLogoutThroughCurrentSessionEndpoint() throws Exception { + AuthService authService = Mockito.mock(AuthService.class); + AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/auth/logout") + .requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-logout-001")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"success\":true"))); + + Mockito.verify(authService).logout("token-logout-001"); + } + + @Test + void shouldChangePasswordThroughCurrentSessionEndpoint() throws Exception { + AuthService authService = Mockito.mock(AuthService.class); + AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/auth/change-password") + .requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-change-001") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "currentPassword": "12345678", + "newPassword": "87654321" + } + """)) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"success\":true"))); + + Mockito.verify(authService).changePassword("token-change-001", "12345678", "87654321"); + } +} diff --git a/src/test/java/com/cisd/tms/modules/auth/controller/CompatAuthControllerTest.java b/src/test/java/com/cisd/tms/modules/auth/controller/CompatAuthControllerTest.java new file mode 100644 index 0000000..1e643f8 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/auth/controller/CompatAuthControllerTest.java @@ -0,0 +1,114 @@ +package com.cisd.tms.modules.auth.controller; + +import com.cisd.tms.common.exception.GlobalExceptionHandler; +import com.cisd.tms.modules.auth.dto.LoginResponse; +import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse; +import com.cisd.tms.modules.auth.service.CompatAuthService; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class CompatAuthControllerTest { + + @Test + void shouldIssueUkeyLoginRandoms() throws Exception { + CompatAuthService compatAuthService = Mockito.mock(CompatAuthService.class); + UkeyLoginRandomResponse response = new UkeyLoginRandomResponse(); + response.setRoleCode("SUPER_ADMIN"); + response.setRandoms(java.util.List.of("RB-1", "RB-2", "RB-3")); + Mockito.when(compatAuthService.issueUkeyLoginRandoms("superadmin", null)).thenReturn(response); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CompatAuthController(compatAuthService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(get("/api/v1/auth/ukey-login/randoms") + .param("role", "superadmin")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"roleCode\":\"SUPER_ADMIN\""))) + .andExpect(content().string(containsString("\"RB-1\""))); + } + + @Test + void shouldLoginWithCompatiblePasswordPayload() throws Exception { + CompatAuthService compatAuthService = Mockito.mock(CompatAuthService.class); + LoginResponse response = new LoginResponse(); + response.setRoleCode("AUDIT_ADMIN"); + response.setAuthLevel("LIMITED"); + response.setToken("token-password-001"); + Mockito.when(compatAuthService.passwordLogin(ArgumentMatchers.any())).thenReturn(response); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CompatAuthController(compatAuthService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/auth/password-login") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "role": "auditadmin", + "password": "12345678" + } + """)) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"token\":\"token-password-001\""))) + .andExpect(content().string(containsString("\"authLevel\":\"LIMITED\""))); + } + + @Test + void shouldLoginWithCompatibleUkeyPayload() throws Exception { + CompatAuthService compatAuthService = Mockito.mock(CompatAuthService.class); + LoginResponse response = new LoginResponse(); + response.setRoleCode("KEY_ADMIN"); + response.setAuthLevel("FULL"); + response.setToken("token-ukey-001"); + Mockito.when(compatAuthService.ukeyLogin(ArgumentMatchers.any())).thenReturn(response); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CompatAuthController(compatAuthService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/auth/ukey-login") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "role": "keyadmin", + "password": "12345678", + "authInfo": [ + { + "pubKey": "PUB-1", + "uid": "1", + "rid": "RID-1", + "ra": "RA-1", + "rb": "RB-1", + "issueSign": "ISSUE-1", + "loginSignData": "LOGIN-DATA-1", + "loginSign": "LOGIN-SIGN-1" + }, + { + "pubKey": "PUB-2", + "uid": "2", + "rid": "RID-2", + "ra": "RA-2", + "rb": "RB-2", + "issueSign": "ISSUE-2", + "loginSignData": "LOGIN-DATA-2", + "loginSign": "LOGIN-SIGN-2" + } + ] + } + """)) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"token\":\"token-ukey-001\""))) + .andExpect(content().string(containsString("\"authLevel\":\"FULL\""))); + } +} diff --git a/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java b/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java new file mode 100644 index 0000000..63314f6 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/auth/security/InternalAuthorizationInterceptorTest.java @@ -0,0 +1,85 @@ +package com.cisd.tms.modules.auth.security; + +import com.cisd.tms.security.internal.InternalApiAuthInterceptor; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.lang.reflect.Method; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.method.HandlerMethod; + +class InternalAuthorizationInterceptorTest { + + @Test + void shouldAllowRequestWhenHandlerHasNoAuthorizationAnnotations() throws Exception { + InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper()); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/public"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, handler("openEndpoint")); + + Assertions.assertTrue(allowed); + } + + @Test + void shouldRejectWhenRoleDoesNotMatchRequiredRole() throws Exception { + InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper()); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/enable"); + request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN"); + request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint")); + + Assertions.assertFalse(allowed); + Assertions.assertEquals(403, response.getStatus()); + Assertions.assertTrue(response.getContentAsString().contains("role not allowed")); + } + + @Test + void shouldRejectWhenAuthLevelDoesNotMatchRequirement() throws Exception { + InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper()); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password"); + request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN"); + request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "LIMITED"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint")); + + Assertions.assertFalse(allowed); + Assertions.assertEquals(403, response.getStatus()); + Assertions.assertTrue(response.getContentAsString().contains("auth level not allowed")); + } + + @Test + void shouldAllowWhenRoleAndAuthLevelMatch() throws Exception { + InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper()); + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password"); + request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN"); + request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint")); + + Assertions.assertTrue(allowed); + } + + private HandlerMethod handler(String methodName) throws NoSuchMethodException { + DemoController controller = new DemoController(); + Method method = DemoController.class.getDeclaredMethod(methodName); + return new HandlerMethod(controller, method); + } + + @SuppressWarnings("unused") + private static class DemoController { + + public void openEndpoint() { + } + + @RequireRole("KEY_ADMIN") + @RequireAuthLevel("FULL") + public void keyAdminFullEndpoint() { + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java new file mode 100644 index 0000000..0c8f99c --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java @@ -0,0 +1,340 @@ +package com.cisd.tms.modules.auth.service; + +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; +import com.cisd.tms.modules.auth.enums.AuthLevel; +import com.cisd.tms.modules.auth.enums.RoleAccountStatus; +import com.cisd.tms.modules.auth.enums.RoleCode; +import com.cisd.tms.modules.auth.repository.RoleAccountRepository; +import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; +import com.cisd.tms.modules.mk.dto.UKeySignDTO; +import com.cisd.tms.modules.mk.dto.UKeySignResult; +import com.cisd.tms.modules.mk.service.LmkService; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class AuthAdminServiceTest { + + private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T03:00:00Z"), ZoneOffset.UTC); + + @Test + void shouldEnableRoleWhenOperatorIsKeyAdminWithFullSession() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + roleAccounts.save(role(RoleCode.AUDIT_ADMIN, RoleAccountStatus.UNENABLED, "OLD-HASH", "OLD-SALT", true)); + + AuthAdminService service = new AuthAdminService( + roleAccounts, + new InMemoryRoleUkeyBindingRepository(), + new FakePasswordHasher(), + FIXED_CLOCK, + new FixedSaltSupplier("salt-001") + ); + + service.enableRole(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode()); + + RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow(); + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), updated.getStatus()); + Assertions.assertTrue(updated.getNeedChangePassword()); + } + + @Test + void shouldRejectEnableRoleWhenOperatorIsNotKeyAdminFull() { + AuthAdminService service = new AuthAdminService( + new InMemoryRoleAccountRepository(), + new InMemoryRoleUkeyBindingRepository(), + new FakePasswordHasher(), + FIXED_CLOCK, + new FixedSaltSupplier("salt-001") + ); + + BizException exception = Assertions.assertThrows( + BizException.class, + () -> service.enableRole(RoleCode.KEY_ADMIN.getCode(), AuthLevel.LIMITED.name(), RoleCode.AUDIT_ADMIN.getCode()) + ); + + Assertions.assertEquals("operation requires KEY_ADMIN FULL session", exception.getMessage()); + } + + @Test + void shouldResetPasswordUnlockRoleAndRequirePasswordChange() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + RoleAccountEntity target = role(RoleCode.OPS_ADMIN, RoleAccountStatus.LOCKED, "OLD-HASH", "OLD-SALT", false); + target.setFailedCount(5); + target.setLockedUntil(LocalDateTime.of(2026, 3, 23, 3, 30)); + roleAccounts.save(target); + + AuthAdminService service = new AuthAdminService( + roleAccounts, + new InMemoryRoleUkeyBindingRepository(), + new FakePasswordHasher(), + FIXED_CLOCK, + new FixedSaltSupplier("salt-002") + ); + + service.resetPassword(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.OPS_ADMIN.getCode()); + + RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.OPS_ADMIN.getCode()).orElseThrow(); + Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), updated.getStatus()); + Assertions.assertEquals(0, updated.getFailedCount()); + Assertions.assertNull(updated.getLockedUntil()); + Assertions.assertEquals("salt-002", updated.getPasswordSalt()); + Assertions.assertEquals("HASH:12345678:salt-002", updated.getPasswordHash()); + Assertions.assertTrue(updated.getNeedChangePassword()); + } + + @Test + void shouldReplaceActiveBindingInSameSlot() { + InMemoryRoleUkeyBindingRepository bindings = new InMemoryRoleUkeyBindingRepository(); + bindings.save(binding(RoleCode.SUPER_ADMIN, 1, "UK-OLD", "PUB-OLD", "SIG-OLD")); + LmkService lmkService = org.mockito.Mockito.mock(LmkService.class); + org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); + + AuthAdminService service = new AuthAdminService( + new InMemoryRoleAccountRepository(), + bindings, + lmkService, + new FakePasswordHasher(), + FIXED_CLOCK, + new FixedSaltSupplier("salt-001") + ); + + service.bindIssuedUkey( + RoleCode.KEY_ADMIN.getCode(), + AuthLevel.FULL.name(), + RoleCode.SUPER_ADMIN.getCode(), + 1, + "UK-NEW", + "PUB-NEW", + "1", + "RID-001", + "SIG-NEW" + ); + + RoleUkeyBindingEntity updated = bindings.findActiveByRoleCodeAndSlotNo(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow(); + Assertions.assertEquals("UK-NEW", updated.getUkeySerial()); + Assertions.assertEquals("PUB-NEW", updated.getUkeyPubkey()); + Assertions.assertEquals("SIG-NEW", updated.getIssuerSign()); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), updated.getBoundAt()); + org.mockito.Mockito.verify(lmkService).verifyIk( + "{\"pubKey\":\"PUB-NEW\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"superadmin\",\"uid\":\"1\",\"rid\":\"RID-001\"}", + "SIG-NEW" + ); + } + + @Test + void shouldRejectBindingWhenSlotExceedsRoleRequirement() { + LmkService lmkService = org.mockito.Mockito.mock(LmkService.class); + AuthAdminService service = new AuthAdminService( + new InMemoryRoleAccountRepository(), + new InMemoryRoleUkeyBindingRepository(), + lmkService, + new FakePasswordHasher(), + FIXED_CLOCK, + new FixedSaltSupplier("salt-001") + ); + + BizException exception = Assertions.assertThrows( + BizException.class, + () -> service.bindIssuedUkey( + RoleCode.KEY_ADMIN.getCode(), + AuthLevel.FULL.name(), + RoleCode.AUDIT_ADMIN.getCode(), + 2, + "UK-001", + "PUB-001", + "1", + "RID-001", + "SIG-001" + ) + ); + + Assertions.assertEquals("slotNo exceeds role ukey requirement", exception.getMessage()); + } + + @Test + void shouldIssueBindingSignWithKeyComponentForSuperAdmin() { + LmkService lmkService = org.mockito.Mockito.mock(LmkService.class); + org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); + org.mockito.Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-001"); + org.mockito.Mockito.when(lmkService.getComponent(1)).thenReturn("COMP-001"); + + AuthAdminService service = new AuthAdminService( + new InMemoryRoleAccountRepository(), + new InMemoryRoleUkeyBindingRepository(), + lmkService, + new FakePasswordHasher(), + FIXED_CLOCK, + new FixedSaltSupplier("salt-001") + ); + UKeySignDTO dto = new UKeySignDTO(); + dto.setPubKey("PUB-001"); + dto.setRole("ignored"); + dto.setUid("1"); + dto.setRid("RID-001"); + dto.setExtra("EXTRA-001"); + + UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto); + + Assertions.assertEquals("ISSUE-SIGN-001", result.getSign()); + Assertions.assertEquals("COMP-001", result.getComponent()); + org.mockito.Mockito.verify(lmkService).signIk( + "{\"pubKey\":\"PUB-001\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"superadmin\",\"uid\":\"1\",\"rid\":\"RID-001\"}" + ); + } + + @Test + void shouldIssueBindingSignAndPreserveExtraForNonSuperAdmin() { + LmkService lmkService = org.mockito.Mockito.mock(LmkService.class); + org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); + org.mockito.Mockito.when(lmkService.signIk(org.mockito.ArgumentMatchers.anyString())).thenReturn("ISSUE-SIGN-002"); + + AuthAdminService service = new AuthAdminService( + new InMemoryRoleAccountRepository(), + new InMemoryRoleUkeyBindingRepository(), + lmkService, + new FakePasswordHasher(), + FIXED_CLOCK, + new FixedSaltSupplier("salt-001") + ); + UKeySignDTO dto = new UKeySignDTO(); + dto.setPubKey("PUB-002"); + dto.setRole("ignored"); + dto.setUid("1"); + dto.setRid("RID-002"); + dto.setExtra("EXTRA-KEEP"); + + UKeySignResult result = service.issueUkeyBindingSign(RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.AUDIT_ADMIN.getCode(), dto); + + Assertions.assertEquals("ISSUE-SIGN-002", result.getSign()); + Assertions.assertEquals("EXTRA-KEEP", result.getComponent()); + } + + private static RoleAccountEntity role( + RoleCode roleCode, + RoleAccountStatus status, + String passwordHash, + String passwordSalt, + boolean needChangePassword + ) { + RoleAccountEntity entity = new RoleAccountEntity(); + entity.setId((long) roleCode.ordinal() + 100); + entity.setRoleCode(roleCode.getCode()); + entity.setDisplayName(roleCode.getDisplayName()); + entity.setRequiredUkeyCount(roleCode.getRequiredUkeyCount()); + entity.setPasswordHash(passwordHash); + entity.setPasswordSalt(passwordSalt); + entity.setStatus(status.name()); + entity.setNeedChangePassword(needChangePassword); + entity.setFailedCount(0); + return entity; + } + + private static RoleUkeyBindingEntity binding( + RoleCode roleCode, + int slotNo, + String serial, + String pubkey, + String issuerSign + ) { + RoleUkeyBindingEntity entity = new RoleUkeyBindingEntity(); + entity.setId((long) slotNo); + entity.setRoleCode(roleCode.getCode()); + entity.setSlotNo(slotNo); + entity.setUkeySerial(serial); + entity.setUkeyPubkey(pubkey); + entity.setIssuerSign(issuerSign); + entity.setStatus("ACTIVE"); + entity.setBoundAt(LocalDateTime.of(2026, 3, 22, 8, 0)); + return entity; + } + + private static class FakePasswordHasher implements PasswordHasher { + + @Override + public String hash(String password, String salt) { + return "HASH:" + password + ":" + salt; + } + + @Override + public boolean matches(String rawPassword, String salt, String expectedHash) { + return hash(rawPassword, salt).equals(expectedHash); + } + } + + private static class FixedSaltSupplier implements java.util.function.Supplier { + private final Deque salts; + + private FixedSaltSupplier(String... salts) { + this.salts = new ArrayDeque<>(List.of(salts)); + } + + @Override + public String get() { + return Optional.ofNullable(salts.pollFirst()).orElse("salt-fallback"); + } + } + + private static class InMemoryRoleAccountRepository implements RoleAccountRepository { + private final Map store = new ConcurrentHashMap<>(); + + @Override + public Optional findByRoleCode(String roleCode) { + return Optional.ofNullable(store.get(roleCode)); + } + + @Override + public void save(RoleAccountEntity entity) { + store.put(entity.getRoleCode(), entity); + } + + @Override + public void update(RoleAccountEntity entity) { + store.put(entity.getRoleCode(), entity); + } + } + + private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository { + private final Map> store = new ConcurrentHashMap<>(); + + @Override + public Optional findActiveByRoleCodeAndSlotNo(String roleCode, Integer slotNo) { + return store.getOrDefault(roleCode, List.of()).stream() + .filter(entity -> slotNo.equals(entity.getSlotNo())) + .filter(entity -> "ACTIVE".equals(entity.getStatus())) + .findFirst(); + } + + @Override + public List findActiveByRoleCode(String roleCode) { + return store.getOrDefault(roleCode, List.of()).stream() + .filter(entity -> "ACTIVE".equals(entity.getStatus())) + .sorted(Comparator.comparing(RoleUkeyBindingEntity::getSlotNo)) + .toList(); + } + + @Override + public void save(RoleUkeyBindingEntity entity) { + store.computeIfAbsent(entity.getRoleCode(), ignored -> new ArrayList<>()).add(entity); + } + + @Override + public void update(RoleUkeyBindingEntity entity) { + List list = store.computeIfAbsent(entity.getRoleCode(), ignored -> new ArrayList<>()); + list.removeIf(item -> item.getId().equals(entity.getId())); + list.add(entity); + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthDomainModelTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthDomainModelTest.java new file mode 100644 index 0000000..77e828f --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthDomainModelTest.java @@ -0,0 +1,31 @@ +package com.cisd.tms.modules.auth.service; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class AuthDomainModelTest { + + @Test + void shouldExposeOnlyFourFixedRoleAccountsWithExpectedUkeyCounts() { + Assertions.assertEquals(4, com.cisd.tms.modules.auth.enums.RoleCode.values().length); + Assertions.assertEquals(3, com.cisd.tms.modules.auth.enums.RoleCode.SUPER_ADMIN.getRequiredUkeyCount()); + Assertions.assertEquals(2, com.cisd.tms.modules.auth.enums.RoleCode.KEY_ADMIN.getRequiredUkeyCount()); + Assertions.assertEquals(1, com.cisd.tms.modules.auth.enums.RoleCode.AUDIT_ADMIN.getRequiredUkeyCount()); + Assertions.assertEquals(1, com.cisd.tms.modules.auth.enums.RoleCode.OPS_ADMIN.getRequiredUkeyCount()); + } + + @Test + void shouldExposeLimitedAndFullAuthLevelsOnly() { + Assertions.assertEquals(2, com.cisd.tms.modules.auth.enums.AuthLevel.values().length); + Assertions.assertEquals("LIMITED", com.cisd.tms.modules.auth.enums.AuthLevel.LIMITED.name()); + Assertions.assertEquals("FULL", com.cisd.tms.modules.auth.enums.AuthLevel.FULL.name()); + } + + @Test + void shouldExposeExpectedRoleAccountStatuses() { + Assertions.assertEquals(3, com.cisd.tms.modules.auth.enums.RoleAccountStatus.values().length); + Assertions.assertEquals("UNENABLED", com.cisd.tms.modules.auth.enums.RoleAccountStatus.UNENABLED.name()); + Assertions.assertEquals("ACTIVE", com.cisd.tms.modules.auth.enums.RoleAccountStatus.ACTIVE.name()); + Assertions.assertEquals("LOCKED", com.cisd.tms.modules.auth.enums.RoleAccountStatus.LOCKED.name()); + } +} diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java new file mode 100644 index 0000000..e66c423 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java @@ -0,0 +1,406 @@ +package com.cisd.tms.modules.auth.service; + +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.auth.dto.LoginRequest; +import com.cisd.tms.modules.auth.dto.LoginResponse; +import com.cisd.tms.modules.auth.entity.AuthSessionEntity; +import com.cisd.tms.modules.auth.entity.AuthUserEntity; +import com.cisd.tms.modules.auth.entity.RoleAccountEntity; +import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; +import com.cisd.tms.modules.auth.enums.AuthLevel; +import com.cisd.tms.modules.auth.enums.RoleAccountStatus; +import com.cisd.tms.modules.auth.enums.RoleCode; +import com.cisd.tms.modules.auth.repository.AuthUserRepository; +import com.cisd.tms.modules.auth.repository.AuthSessionRepository; +import com.cisd.tms.modules.auth.repository.RoleAccountRepository; +import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class AuthServiceTest { + + private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T02:00:00Z"), ZoneOffset.UTC); + + @Test + void shouldCreateLimitedSessionForPasswordOnlyLogin() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository(); + roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, true)); + + AuthService service = new AuthService( + roleAccounts, + new InMemoryLegacyAuthUserRepository(), + sessions, + ukeyBindings, + new FakePasswordHasher(), + FIXED_CLOCK, + () -> "token-limited-001" + ); + + LoginRequest request = new LoginRequest(); + request.setRoleCode(RoleCode.AUDIT_ADMIN.getCode()); + request.setPassword("12345678"); + + LoginResponse response = service.login(request); + + Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getRoleCode()); + Assertions.assertEquals(AuthLevel.LIMITED.name(), response.getAuthLevel()); + Assertions.assertEquals("token-limited-001", response.getToken()); + Assertions.assertEquals("2026-03-23T02:10Z", response.getExpiresAt()); + Assertions.assertTrue(response.getNeedChangePassword()); + Assertions.assertEquals(1, sessions.count()); + AuthSessionEntity session = sessions.findBySessionToken("token-limited-001").orElseThrow(); + Assertions.assertEquals(AuthLevel.LIMITED.name(), session.getAuthLevel()); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), session.getIssuedAt()); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), session.getExpiresAt()); + } + + @Test + void shouldCreateFullSessionWhenEnoughUkeysAreProvided() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository(); + roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "HASH:12345678:SALT-K", "SALT-K", 0, false)); + ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1")); + ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 2, "UK-2")); + + AuthService service = new AuthService( + roleAccounts, + new InMemoryLegacyAuthUserRepository(), + sessions, + ukeyBindings, + new FakePasswordHasher(), + FIXED_CLOCK, + () -> "token-full-001" + ); + + LoginRequest request = new LoginRequest(); + request.setRoleCode(RoleCode.KEY_ADMIN.getCode()); + request.setPassword("12345678"); + request.setUkeySerials(List.of("UK-1", "UK-2")); + + LoginResponse response = service.login(request); + + Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel()); + Assertions.assertEquals("token-full-001", response.getToken()); + Assertions.assertFalse(response.getNeedChangePassword()); + } + + @Test + void shouldRejectLoginWhenRoleIsUnenabled() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + roleAccounts.save(role(RoleCode.OPS_ADMIN, RoleAccountStatus.UNENABLED, "HASH:12345678:SALT-O", "SALT-O", 0, true, null)); + + AuthService service = new AuthService( + roleAccounts, + new InMemoryLegacyAuthUserRepository(), + new InMemoryAuthSessionRepository(), + new InMemoryRoleUkeyBindingRepository(), + new FakePasswordHasher(), + FIXED_CLOCK, + () -> "unused" + ); + + LoginRequest request = new LoginRequest(); + request.setRoleCode(RoleCode.OPS_ADMIN.getCode()); + request.setPassword("12345678"); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.login(request)); + + Assertions.assertEquals("role is not enabled", exception.getMessage()); + } + + @Test + void shouldIncrementFailedCountAndLockRoleAfterFiveWrongPasswords() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + RoleAccountEntity role = activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 4, false); + roleAccounts.save(role); + + AuthService service = new AuthService( + roleAccounts, + new InMemoryLegacyAuthUserRepository(), + new InMemoryAuthSessionRepository(), + new InMemoryRoleUkeyBindingRepository(), + new FakePasswordHasher(), + FIXED_CLOCK, + () -> "unused" + ); + + LoginRequest request = new LoginRequest(); + request.setRoleCode(RoleCode.AUDIT_ADMIN.getCode()); + request.setPassword("wrong-password"); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.login(request)); + + Assertions.assertEquals("role account is locked", exception.getMessage()); + RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.AUDIT_ADMIN.getCode()).orElseThrow(); + Assertions.assertEquals(RoleAccountStatus.LOCKED.name(), updated.getStatus()); + Assertions.assertEquals(5, updated.getFailedCount()); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), updated.getLockedUntil()); + } + + @Test + void shouldRejectFullLoginWhenUkeyCountDoesNotMeetRoleRequirement() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository(); + roleAccounts.save(activeRole(RoleCode.SUPER_ADMIN, "HASH:12345678:SALT-S", "SALT-S", 0, false)); + ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 1, "UK-1")); + ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 2, "UK-2")); + ukeyBindings.save(activeBinding(RoleCode.SUPER_ADMIN, 3, "UK-3")); + + AuthService service = new AuthService( + roleAccounts, + new InMemoryLegacyAuthUserRepository(), + new InMemoryAuthSessionRepository(), + ukeyBindings, + new FakePasswordHasher(), + FIXED_CLOCK, + () -> "unused" + ); + + LoginRequest request = new LoginRequest(); + request.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); + request.setPassword("12345678"); + request.setUkeySerials(List.of("UK-1", "UK-2")); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.login(request)); + + Assertions.assertEquals("ukey count does not satisfy role requirement", exception.getMessage()); + } + + @Test + void shouldReturnCurrentFixedRoleInfoForMe() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, false)); + sessions.save(session("token-me-001", RoleCode.AUDIT_ADMIN.getCode(), AuthLevel.LIMITED.name())); + + AuthService service = new AuthService( + roleAccounts, + new InMemoryLegacyAuthUserRepository(), + sessions, + new InMemoryRoleUkeyBindingRepository(), + new FakePasswordHasher(), + FIXED_CLOCK, + () -> "unused" + ); + + var response = service.me(RoleCode.AUDIT_ADMIN.getCode(), "token-me-001"); + + Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getUsername()); + Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getDisplayName(), response.getDisplayName()); + Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), response.getRole()); + Assertions.assertEquals(AuthLevel.LIMITED.name(), response.getAuthLevel()); + Assertions.assertFalse(response.getNeedChangePassword()); + } + + @Test + void shouldChangePasswordAndClearNeedChangePassword() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "HASH:12345678:SALT-K", "SALT-K", 0, true)); + sessions.save(session("token-change-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name())); + + AuthService service = new AuthService( + roleAccounts, + new InMemoryLegacyAuthUserRepository(), + sessions, + new InMemoryRoleUkeyBindingRepository(), + new FakePasswordHasher(), + FIXED_CLOCK, + () -> "unused" + ); + + service.changePassword("token-change-001", "12345678", "87654321"); + + RoleAccountEntity updated = roleAccounts.findByRoleCode(RoleCode.KEY_ADMIN.getCode()).orElseThrow(); + Assertions.assertFalse(updated.getNeedChangePassword()); + Assertions.assertNotEquals("SALT-K", updated.getPasswordSalt()); + Assertions.assertEquals("HASH:87654321:" + updated.getPasswordSalt(), updated.getPasswordHash()); + } + + @Test + void shouldLogoutSession() { + InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + roleAccounts.save(activeRole(RoleCode.OPS_ADMIN, "HASH:12345678:SALT-O", "SALT-O", 0, false)); + sessions.save(session("token-logout-001", RoleCode.OPS_ADMIN.getCode(), AuthLevel.LIMITED.name())); + + AuthService service = new AuthService( + roleAccounts, + new InMemoryLegacyAuthUserRepository(), + sessions, + new InMemoryRoleUkeyBindingRepository(), + new FakePasswordHasher(), + FIXED_CLOCK, + () -> "unused" + ); + + service.logout("token-logout-001"); + + AuthSessionEntity session = sessions.findBySessionToken("token-logout-001").orElseThrow(); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), session.getLogoutAt()); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), session.getExpiresAt()); + } + + private static RoleAccountEntity activeRole( + RoleCode roleCode, + String passwordHash, + String salt, + int failedCount, + boolean needChangePassword + ) { + return role(roleCode, RoleAccountStatus.ACTIVE, passwordHash, salt, failedCount, needChangePassword, null); + } + + private static RoleAccountEntity role( + RoleCode roleCode, + RoleAccountStatus status, + String passwordHash, + String salt, + int failedCount, + boolean needChangePassword, + LocalDateTime lockedUntil + ) { + RoleAccountEntity entity = new RoleAccountEntity(); + entity.setId((long) roleCode.ordinal() + 1); + entity.setRoleCode(roleCode.getCode()); + entity.setDisplayName(roleCode.getDisplayName()); + entity.setRequiredUkeyCount(roleCode.getRequiredUkeyCount()); + entity.setPasswordHash(passwordHash); + entity.setPasswordSalt(salt); + entity.setStatus(status.name()); + entity.setNeedChangePassword(needChangePassword); + entity.setFailedCount(failedCount); + entity.setLockedUntil(lockedUntil); + return entity; + } + + private static RoleUkeyBindingEntity activeBinding(RoleCode roleCode, int slotNo, String serial) { + RoleUkeyBindingEntity entity = new RoleUkeyBindingEntity(); + entity.setId((long) slotNo); + entity.setRoleCode(roleCode.getCode()); + entity.setSlotNo(slotNo); + entity.setUkeySerial(serial); + entity.setStatus("ACTIVE"); + return entity; + } + + private static class FakePasswordHasher implements PasswordHasher { + + @Override + public String hash(String password, String salt) { + return "HASH:" + password + ":" + salt; + } + + @Override + public boolean matches(String rawPassword, String salt, String expectedHash) { + return hash(rawPassword, salt).equals(expectedHash); + } + } + + private static class InMemoryRoleAccountRepository implements RoleAccountRepository { + private final Map store = new ConcurrentHashMap<>(); + + @Override + public Optional findByRoleCode(String roleCode) { + return Optional.ofNullable(store.get(roleCode)); + } + + @Override + public void save(RoleAccountEntity entity) { + store.put(entity.getRoleCode(), entity); + } + + @Override + public void update(RoleAccountEntity entity) { + store.put(entity.getRoleCode(), entity); + } + } + + private static class InMemoryLegacyAuthUserRepository implements AuthUserRepository { + + @Override + public Optional findByUsername(String username) { + return Optional.empty(); + } + } + + private static class InMemoryAuthSessionRepository implements AuthSessionRepository { + private final Map store = new ConcurrentHashMap<>(); + + @Override + public Optional findBySessionToken(String sessionToken) { + return Optional.ofNullable(store.get(sessionToken)); + } + + @Override + public void save(AuthSessionEntity entity) { + store.put(entity.getSessionToken(), entity); + } + + @Override + public void update(AuthSessionEntity entity) { + store.put(entity.getSessionToken(), entity); + } + + private int count() { + return store.size(); + } + } + + private static class InMemoryRoleUkeyBindingRepository implements RoleUkeyBindingRepository { + private final Map> store = new ConcurrentHashMap<>(); + + @Override + public Optional findActiveByRoleCodeAndSlotNo(String roleCode, Integer slotNo) { + return store.getOrDefault(roleCode, List.of()).stream() + .filter(entity -> slotNo.equals(entity.getSlotNo())) + .filter(entity -> "ACTIVE".equals(entity.getStatus())) + .findFirst(); + } + + @Override + public List findActiveByRoleCode(String roleCode) { + return store.getOrDefault(roleCode, List.of()).stream() + .filter(entity -> "ACTIVE".equals(entity.getStatus())) + .sorted(Comparator.comparing(RoleUkeyBindingEntity::getSlotNo)) + .toList(); + } + + @Override + public void save(RoleUkeyBindingEntity entity) { + store.computeIfAbsent(entity.getRoleCode(), ignored -> new ArrayList<>()).add(entity); + } + + @Override + public void update(RoleUkeyBindingEntity entity) { + List list = store.computeIfAbsent(entity.getRoleCode(), ignored -> new ArrayList<>()); + list.removeIf(item -> item.getId().equals(entity.getId())); + list.add(entity); + } + } + + private static AuthSessionEntity session(String token, String roleCode, String authLevel) { + AuthSessionEntity entity = new AuthSessionEntity(); + entity.setId((long) (token.hashCode() & Integer.MAX_VALUE)); + entity.setSessionToken(token); + entity.setRoleCode(roleCode); + entity.setAuthLevel(authLevel); + entity.setIssuedAt(LocalDateTime.of(2026, 3, 23, 1, 50)); + entity.setLastActiveAt(LocalDateTime.of(2026, 3, 23, 1, 58)); + entity.setExpiresAt(LocalDateTime.of(2026, 3, 23, 2, 8)); + return entity; + } +} diff --git a/src/test/java/com/cisd/tms/modules/auth/service/CompatAuthServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/CompatAuthServiceTest.java new file mode 100644 index 0000000..457592b --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/auth/service/CompatAuthServiceTest.java @@ -0,0 +1,198 @@ +package com.cisd.tms.modules.auth.service; + +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.auth.dto.CompatPasswordLoginRequest; +import com.cisd.tms.modules.auth.dto.CompatUkeyLoginRequest; +import com.cisd.tms.modules.auth.dto.LoginResponse; +import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse; +import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity; +import com.cisd.tms.modules.auth.enums.RoleCode; +import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository; +import com.cisd.tms.modules.mk.enums.MasterKeyStatus; +import com.cisd.tms.modules.mk.service.LmkService; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +class CompatAuthServiceTest { + + @Test + void shouldIssueRoleScopedRandomsForUkeyLogin() { + AuthService authService = Mockito.mock(AuthService.class); + RoleUkeyBindingRepository bindings = Mockito.mock(RoleUkeyBindingRepository.class); + LmkService lmkService = Mockito.mock(LmkService.class); + UkeyLoginRandomService randomService = Mockito.mock(UkeyLoginRandomService.class); + CompatUkeyVerifier verifier = Mockito.mock(CompatUkeyVerifier.class); + Mockito.when(randomService.issue(RoleCode.SUPER_ADMIN.getCode(), 3)).thenReturn(List.of("RB-1", "RB-2", "RB-3")); + + CompatAuthService service = new CompatAuthService(authService, bindings, lmkService, randomService, verifier, new ObjectMapper()); + + UkeyLoginRandomResponse response = service.issueUkeyLoginRandoms("superadmin", null); + + Assertions.assertEquals(RoleCode.SUPER_ADMIN.getCode(), response.getRoleCode()); + Assertions.assertEquals(List.of("RB-1", "RB-2", "RB-3"), response.getRandoms()); + } + + @Test + void shouldDelegateCompatiblePasswordLoginIntoCurrentRoleLoginFlow() { + AuthService authService = Mockito.mock(AuthService.class); + RoleUkeyBindingRepository bindings = Mockito.mock(RoleUkeyBindingRepository.class); + LmkService lmkService = Mockito.mock(LmkService.class); + UkeyLoginRandomService randomService = Mockito.mock(UkeyLoginRandomService.class); + CompatUkeyVerifier verifier = Mockito.mock(CompatUkeyVerifier.class); + Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001")); + LoginResponse loginResponse = new LoginResponse(); + loginResponse.setRoleCode(RoleCode.AUDIT_ADMIN.getCode()); + loginResponse.setAuthLevel("LIMITED"); + loginResponse.setToken("token-password-001"); + Mockito.when(authService.login(ArgumentMatchers.any())).thenReturn(loginResponse); + + CompatAuthService service = new CompatAuthService(authService, bindings, lmkService, randomService, verifier, new ObjectMapper()); + CompatPasswordLoginRequest request = new CompatPasswordLoginRequest(); + request.setRole("auditadmin"); + request.setPassword("12345678"); + + LoginResponse response = service.passwordLogin(request); + + Assertions.assertEquals("token-password-001", response.getToken()); + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.cisd.tms.modules.auth.dto.LoginRequest.class); + Mockito.verify(authService).login(captor.capture()); + Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), captor.getValue().getRoleCode()); + Assertions.assertEquals("12345678", captor.getValue().getPassword()); + Assertions.assertNull(captor.getValue().getUkeySerials()); + } + + @Test + void shouldRejectCompatiblePasswordLoginWhenMasterKeyIsNotReady() { + AuthService authService = Mockito.mock(AuthService.class); + RoleUkeyBindingRepository bindings = Mockito.mock(RoleUkeyBindingRepository.class); + LmkService lmkService = Mockito.mock(LmkService.class); + UkeyLoginRandomService randomService = Mockito.mock(UkeyLoginRandomService.class); + CompatUkeyVerifier verifier = Mockito.mock(CompatUkeyVerifier.class); + Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.ABNORMAL.getDetail()); + + CompatAuthService service = new CompatAuthService(authService, bindings, lmkService, randomService, verifier, new ObjectMapper()); + CompatPasswordLoginRequest request = new CompatPasswordLoginRequest(); + request.setRole("auditadmin"); + request.setPassword("12345678"); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.passwordLogin(request)); + + Assertions.assertEquals("master key is not ready", exception.getMessage()); + Mockito.verifyNoInteractions(authService); + } + + @Test + void shouldVerifyIssuedUkeysRandomsAndLoginSignsBeforeCreatingFullSession() { + AuthService authService = Mockito.mock(AuthService.class); + RoleUkeyBindingRepository bindings = Mockito.mock(RoleUkeyBindingRepository.class); + LmkService lmkService = Mockito.mock(LmkService.class); + UkeyLoginRandomService randomService = Mockito.mock(UkeyLoginRandomService.class); + CompatUkeyVerifier verifier = Mockito.mock(CompatUkeyVerifier.class); + Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001")); + Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); + Mockito.when(bindings.findActiveByRoleCode(RoleCode.KEY_ADMIN.getCode())).thenReturn(List.of( + binding(1, "UK-1", "PUB-1"), + binding(2, "UK-2", "PUB-2") + )); + LoginResponse loginResponse = new LoginResponse(); + loginResponse.setRoleCode(RoleCode.KEY_ADMIN.getCode()); + loginResponse.setAuthLevel("FULL"); + loginResponse.setToken("token-full-001"); + Mockito.when(authService.login(ArgumentMatchers.any())).thenReturn(loginResponse); + + CompatAuthService service = new CompatAuthService(authService, bindings, lmkService, randomService, verifier, new ObjectMapper()); + CompatUkeyLoginRequest request = new CompatUkeyLoginRequest(); + request.setRole("keyadmin"); + request.setPassword("12345678"); + request.setAuthInfo(List.of( + authInfo("PUB-1", "1", "RID-1", "RA-1", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1"), + authInfo("PUB-2", "2", "RID-2", "RA-2", "RB-2", "ISSUE-2", "LOGIN-DATA-2", "LOGIN-SIGN-2") + )); + + LoginResponse response = service.ukeyLogin(request); + + Assertions.assertEquals("token-full-001", response.getToken()); + Mockito.verify(randomService).assertIssued(RoleCode.KEY_ADMIN.getCode(), List.of("RB-1", "RB-2")); + Mockito.verify(verifier).verifyIssuedBinding( + "{\"pubKey\":\"PUB-1\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"keyadmin\",\"uid\":\"1\",\"rid\":\"RID-1\"}", + "ISSUE-1" + ); + Mockito.verify(verifier).verifyIssuedBinding( + "{\"pubKey\":\"PUB-2\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"keyadmin\",\"uid\":\"2\",\"rid\":\"RID-2\"}", + "ISSUE-2" + ); + Mockito.verify(verifier).verifyLoginSignature("PUB-1", "LOGIN-DATA-1", "LOGIN-SIGN-1"); + Mockito.verify(verifier).verifyLoginSignature("PUB-2", "LOGIN-DATA-2", "LOGIN-SIGN-2"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(com.cisd.tms.modules.auth.dto.LoginRequest.class); + Mockito.verify(authService).login(captor.capture()); + Assertions.assertEquals(RoleCode.KEY_ADMIN.getCode(), captor.getValue().getRoleCode()); + Assertions.assertEquals("12345678", captor.getValue().getPassword()); + Assertions.assertEquals(List.of("UK-1", "UK-2"), captor.getValue().getUkeySerials()); + } + + @Test + void shouldRejectUkeyLoginWhenRequestPubkeyIsNotBoundToRole() { + AuthService authService = Mockito.mock(AuthService.class); + RoleUkeyBindingRepository bindings = Mockito.mock(RoleUkeyBindingRepository.class); + LmkService lmkService = Mockito.mock(LmkService.class); + UkeyLoginRandomService randomService = Mockito.mock(UkeyLoginRandomService.class); + CompatUkeyVerifier verifier = Mockito.mock(CompatUkeyVerifier.class); + Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001")); + Mockito.when(bindings.findActiveByRoleCode(RoleCode.AUDIT_ADMIN.getCode())).thenReturn(List.of( + binding(1, "UK-1", "PUB-BOUND") + )); + + CompatAuthService service = new CompatAuthService(authService, bindings, lmkService, randomService, verifier, new ObjectMapper()); + CompatUkeyLoginRequest request = new CompatUkeyLoginRequest(); + request.setRole("auditadmin"); + request.setPassword("12345678"); + request.setAuthInfo(List.of(authInfo("PUB-OTHER", "1", "RID-1", "RA-1", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1"))); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.ukeyLogin(request)); + + Assertions.assertEquals("ukey auth info does not match bound role", exception.getMessage()); + Mockito.verifyNoInteractions(authService); + } + + private static RoleUkeyBindingEntity binding(int slotNo, String serial, String pubkey) { + RoleUkeyBindingEntity entity = new RoleUkeyBindingEntity(); + entity.setId((long) slotNo); + entity.setRoleCode(RoleCode.KEY_ADMIN.getCode()); + entity.setSlotNo(slotNo); + entity.setUkeySerial(serial); + entity.setUkeyPubkey(pubkey); + entity.setStatus("ACTIVE"); + return entity; + } + + private static CompatUkeyLoginRequest.LoginAuthInfo authInfo( + String pubKey, + String uid, + String rid, + String ra, + String rb, + String issueSign, + String loginSignData, + String loginSign + ) { + CompatUkeyLoginRequest.LoginAuthInfo info = new CompatUkeyLoginRequest.LoginAuthInfo(); + info.setPubKey(pubKey); + info.setUid(uid); + info.setRid(rid); + info.setRa(ra); + info.setRb(rb); + info.setIssueSign(issueSign); + info.setLoginSignData(loginSignData); + info.setLoginSign(loginSign); + return info; + } +} diff --git a/src/test/java/com/cisd/tms/modules/device/controller/CryptoCardControllerTest.java b/src/test/java/com/cisd/tms/modules/device/controller/CryptoCardControllerTest.java new file mode 100644 index 0000000..727312f --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/device/controller/CryptoCardControllerTest.java @@ -0,0 +1,201 @@ +package com.cisd.tms.modules.device.controller; + +import com.cisd.tms.common.exception.GlobalExceptionHandler; +import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; +import com.cisd.tms.integration.crypto.pcie.model.DeviceStatusResult; +import com.cisd.tms.integration.crypto.pcie.model.LoginStatusResult; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import org.mockito.ArgumentCaptor; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class CryptoCardControllerTest { + + @Test + void shouldNotKeepNestedRequestTypesInController() { + long nestedRequestTypes = java.util.Arrays.stream(CryptoCardController.class.getDeclaredClasses()) + .filter(type -> type.getSimpleName().endsWith("InternalRequest")) + .count(); + + assertEquals(0, nestedRequestTypes); + } + + @Test + void shouldNotKeepCryptoHelperMethodsInController() { + java.util.Set helperMethods = java.util.Arrays.stream(CryptoCardController.class.getDeclaredMethods()) + .map(java.lang.reflect.Method::getName) + .collect(java.util.stream.Collectors.toSet()); + + assertFalse(helperMethods.contains("toSymmetricCryptoRequest")); + assertFalse(helperMethods.contains("toSymmetricKekCryptoRequest")); + assertFalse(helperMethods.contains("toGenerateKeyPairRequest")); + assertFalse(helperMethods.contains("toImportKeyPairRequest")); + assertFalse(helperMethods.contains("decodeNullableBase64")); + assertFalse(helperMethods.contains("decodeBase64")); + assertFalse(helperMethods.contains("hexPayload")); + assertFalse(helperMethods.contains("lmkComponentPayload")); + assertFalse(helperMethods.contains("toLmkComponentBytes")); + assertFalse(helperMethods.contains("normalizeBinary")); + } + + @Test + void shouldReturnDeviceStatusAsHex() throws Exception { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + DeviceStatusResult result = new DeviceStatusResult(); + result.setLength(3); + result.setStatusData(new byte[]{0x01, 0x23, (byte) 0xFF}); + result.setFsmState(0); + result.setFsmStateText("就绪状态"); + result.setCommit(0x00018219); + result.setCommitHex("0x00018219"); + result.setFrequencyMhz(500); + result.setFrequencyText("500M"); + result.setPcieSpeed(2); + result.setPcieWidth(8); + result.setPcieInfoText("GEN2 X8"); + result.setBoardTempC(44.44d); + result.setBoardTempText("44.44 °C"); + result.setBoardVoltageV(12.18d); + result.setBoardVoltageText("12.18 V"); + result.setFsTotalKb(47); + result.setFsTotalText("47 KB"); + result.setFsFreeKb(44); + result.setFsFreeText("44 KB"); + Mockito.when(pcieCryptoService.getDeviceStatus()).thenReturn(result); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CryptoCardController(pcieCryptoService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(get("/api/v1/device/crypto/device/status")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"length\":3"))) + .andExpect(content().string(containsString("\"statusHex\":\"0123ff\""))) + .andExpect(content().string(containsString("\"fsmStateText\":\"就绪状态\""))) + .andExpect(content().string(containsString("\"commitHex\":\"0x00018219\""))) + .andExpect(content().string(containsString("\"pcieInfoText\":\"GEN2 X8\""))) + .andExpect(content().string(containsString("\"boardTempText\":\"44.44 °C\""))) + .andExpect(content().string(containsString("\"boardVoltageText\":\"12.18 V\""))) + .andExpect(content().string(containsString("\"fsTotalText\":\"47 KB\""))) + .andExpect(content().string(containsString("\"fsFreeText\":\"44 KB\""))); + } + + @Test + void shouldExportKekComponentAsHex() throws Exception { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + BackupDataResult result = new BackupDataResult(); + result.setLength(2); + result.setData(new byte[]{0x55, (byte) 0xAA}); + Mockito.when(pcieCryptoService.exportKekComponent(Mockito.any())).thenReturn(result); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CryptoCardController(pcieCryptoService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/device/crypto/kek/component/export") + .contentType("application/json") + .content(""" + { + "keyIndex": 1, + "componentNum": 2, + "componentSize": 32 + } + """)) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"length\":2"))) + .andExpect(content().string(containsString("\"componentHex\":\"55aa\""))); + } + + @Test + void shouldBackupLmkExAsHex() throws Exception { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + BackupDataResult result = new BackupDataResult(); + result.setLength(56); + result.setData(new byte[]{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40 + }); + Mockito.when(pcieCryptoService.backupLmkEx()).thenReturn(result); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CryptoCardController(pcieCryptoService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/device/crypto/lmk/backup-ex")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"length\":56"))) + .andExpect(content().string(containsString("\"lmkMacHex\":\"0102030405060708\""))) + .andExpect(content().string(containsString("\"components\":{\"1\":\"1112131415161718191a1b1c1d1e1f20\",\"2\":\"2122232425262728292a2b2c2d2e2f30\",\"3\":\"3132333435363738393a3b3c3d3e3f40\"}"))); + } + + @Test + void shouldRecoverLmkExFromStructuredHexPayload() throws Exception { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CryptoCardController(pcieCryptoService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/device/crypto/lmk/recover-ex") + .contentType("application/json") + .content(""" + { + "lmkMacHex": "0102030405060708", + "components": { + "1": "1112131415161718191a1b1c1d1e1f20", + "2": "2122232425262728292a2b2c2d2e2f30", + "3": "3132333435363738393a3b3c3d3e3f40" + } + } + """)) + .andExpect(status().isOk()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(byte[].class); + Mockito.verify(pcieCryptoService).recoverLmkEx(captor.capture()); + assertArrayEquals(new byte[]{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, + 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40 + }, captor.getValue()); + } + + @Test + void shouldReturnUkeyLoginStatus() throws Exception { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + LoginStatusResult result = new LoginStatusResult(); + result.setAdmin1(1); + result.setAdmin2(0); + result.setAdmin3(1); + Mockito.when(pcieCryptoService.loginStatus()).thenReturn(result); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CryptoCardController(pcieCryptoService)) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(get("/api/v1/device/crypto/ukey/login-status")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"admin1\":1"))) + .andExpect(content().string(containsString("\"admin2\":0"))) + .andExpect(content().string(containsString("\"admin3\":1"))); + } +} diff --git a/src/test/java/com/cisd/tms/modules/device/service/DeviceProfileServiceTest.java b/src/test/java/com/cisd/tms/modules/device/service/DeviceProfileServiceTest.java new file mode 100644 index 0000000..e4f862e --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/device/service/DeviceProfileServiceTest.java @@ -0,0 +1,484 @@ +package com.cisd.tms.modules.device.service; + +import com.cisd.tms.common.config.properties.CisdPresetProperties; +import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult; +import com.cisd.tms.integration.crypto.pcie.service.MockPcieCryptoService; +import com.cisd.tms.modules.device.config.DeviceProfileProperties; +import com.cisd.tms.modules.device.dto.DeviceProfileResponse; +import com.cisd.tms.modules.device.dto.DeviceProfileSoftwareItem; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.device.support.SystemHardwareProbe; +import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe; +import com.cisd.tms.modules.init.config.InitCommandProfileService; +import com.cisd.tms.modules.init.config.InitExecutorProperties; +import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; +import com.cisd.tms.modules.init.dto.InitPreviewRequest; +import com.cisd.tms.modules.init.entity.InitTaskEntity; +import com.cisd.tms.modules.init.entity.InitTaskStepEntity; +import com.cisd.tms.modules.init.executor.InitStepExecutionResult; +import com.cisd.tms.modules.init.executor.InitStepExecutor; +import com.cisd.tms.modules.init.repository.InitTaskRepository; +import com.cisd.tms.modules.init.repository.InitTaskStepRepository; +import com.cisd.tms.modules.init.service.InitService; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class DeviceProfileServiceTest { + + @Test + void shouldReturnInitializedSoftwareProfileForStandardRabbitmqTlqMode() { + InitService initService = newInitService("INDIRECT"); + InitPreviewRequest request = validInitRequest("RABBITMQ_TLQ"); + createSuccessfulInitTask(initService, request); + InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository(); + versionRepository.save(component("RABBITMQ", "消息队列", "RabbitMQ V3.8.21")); + versionRepository.save(component("TLQ", "消息队列", "tonglink/Q V8.1.11")); + versionRepository.save(component("TIDB", "数据库", "TiDB V8.1.0")); + versionRepository.save(component("NGINX", "Nginx", "Nginx V1.20")); + versionRepository.save(component("APP", "标准收发器间参版", "标准收发器间参版 V1.3.2")); + + DeviceProfileService service = new DeviceProfileService( + initService, + cryptoService(), + profileProperties(), + hardwareProbe(), + softwareProbe(Map.of( + "OS", "Kylin V10 SP1", + "TMS", "SYD TMS V1.00" + )), + versionRepository, + preset("INDIRECT") + ); + + DeviceProfileResponse response = service.profile(); + + Assertions.assertEquals("SYD7108", response.getDevice().getModel()); + Assertions.assertEquals("跨境支付终端一体机", response.getDevice().getName()); + Assertions.assertEquals("1234567890ABCDEF", response.getDevice().getSerialNumber()); + Assertions.assertEquals("V2.50", response.getDevice().getDeviceVersion()); + Assertions.assertEquals("SYD1408加密卡", response.getHardware().getCryptoCardName()); + Assertions.assertEquals("Hygon C86-3G (OPN:3350)", response.getHardware().getCpuModel()); + Assertions.assertEquals("32GB", response.getHardware().getMemoryTotal()); + Assertions.assertEquals("953.87GB", response.getHardware().getDiskTotal()); + Assertions.assertEquals( + List.of("OS", "TMS", "RABBITMQ", "TLQ", "TIDB", "NGINX", "APP"), + response.getSoftware().stream().map(DeviceProfileSoftwareItem::getComponentCode).toList() + ); + } + + @Test + void shouldReturnOnlyBaseSoftwareWhenNoSuccessfulInitSnapshotExists() { + DeviceProfileService service = new DeviceProfileService( + newInitService("ENTERPRISE"), + cryptoService(), + profileProperties(), + hardwareProbe(), + softwareProbe(Map.of( + "OS", "Kylin V10 SP1", + "TMS", "SYD TMS V1.00" + )), + new InMemoryDeviceSoftwareVersionRepository(), + preset("ENTERPRISE") + ); + + DeviceProfileResponse response = service.profile(); + + Assertions.assertEquals( + List.of("OS", "TMS"), + response.getSoftware().stream().map(DeviceProfileSoftwareItem::getComponentCode).toList() + ); + } + + @Test + void shouldFallbackToVersionCacheWhenVersionRecordExists() { + InitService initService = newInitService("ENTERPRISE"); + createSuccessfulInitTask(initService, validInitRequest("RABBITMQ")); + + InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository(); + DeviceSoftwareVersionEntity cached = new DeviceSoftwareVersionEntity(); + cached.setComponentCode("NGINX"); + cached.setComponentName("Nginx"); + cached.setCurrentVersion("Nginx V1.20"); + versionRepository.saveOrUpdate(cached); + + DeviceProfileService service = new DeviceProfileService( + initService, + cryptoService(), + profileProperties(), + hardwareProbe(), + softwareProbe(Map.of( + "OS", "Kylin V10 SP1", + "TMS", "SYD TMS V1.00" + )), + versionRepository, + preset("ENTERPRISE") + ); + + DeviceProfileResponse response = service.profile(); + + DeviceProfileSoftwareItem nginx = response.getSoftware().stream() + .filter(item -> "NGINX".equals(item.getComponentCode())) + .findFirst() + .orElseThrow(); + Assertions.assertEquals("Nginx V1.20", nginx.getVersion()); + } + + @Test + void shouldKeepInitializedSoftwareVisibleWhenVersionIsUnknown() { + InitService initService = newInitService("ENTERPRISE"); + createSuccessfulInitTask(initService, validInitRequest("RABBITMQ")); + + DeviceProfileService service = new DeviceProfileService( + initService, + cryptoService(), + profileProperties(), + hardwareProbe(), + softwareProbe(Map.of( + "OS", "Kylin V10 SP1", + "TMS", "SYD TMS V1.00" + )), + new InMemoryDeviceSoftwareVersionRepository(), + preset("ENTERPRISE") + ); + + DeviceProfileResponse response = service.profile(); + + Assertions.assertEquals( + List.of("OS", "TMS", "RABBITMQ", "TIDB", "NGINX", "APP"), + response.getSoftware().stream().map(DeviceProfileSoftwareItem::getComponentCode).toList() + ); + DeviceProfileSoftwareItem rabbitmq = response.getSoftware().stream() + .filter(item -> "RABBITMQ".equals(item.getComponentCode())) + .findFirst() + .orElseThrow(); + Assertions.assertEquals("", rabbitmq.getVersion()); + } + + @Test + void shouldFallbackToConfiguredHardwareDefaultsWhenProbeReturnsBlank() { + DeviceProfileProperties properties = profileProperties(); + properties.setCpuModelDefault("Hygon C86-3G (OPN:3350)"); + properties.setMemoryTotalDefault("16GB"); + properties.setDiskTotalDefault("256G"); + + DeviceProfileService service = new DeviceProfileService( + newInitService("ENTERPRISE"), + cryptoService(), + properties, + blankHardwareProbe(), + softwareProbe(Map.of( + "OS", "Kylin V10 SP1", + "TMS", "SYD TMS V1.00" + )), + new InMemoryDeviceSoftwareVersionRepository(), + preset("ENTERPRISE") + ); + + DeviceProfileResponse response = service.profile(); + + Assertions.assertEquals("Hygon C86-3G (OPN:3350)", response.getHardware().getCpuModel()); + Assertions.assertEquals("16GB", response.getHardware().getMemoryTotal()); + Assertions.assertEquals("256G", response.getHardware().getDiskTotal()); + } + + private static DeviceProfileProperties profileProperties() { + DeviceProfileProperties properties = new DeviceProfileProperties(); + properties.setModel("SYD7108"); + properties.setName("跨境支付终端一体机"); + return properties; + } + + private static SystemHardwareProbe hardwareProbe() { + return new SystemHardwareProbe() { + @Override + public String cpuModel() { + return "Hygon C86-3G (OPN:3350)"; + } + + @Override + public String memoryTotal() { + return "32GB"; + } + + @Override + public String diskTotal() { + return "953.87GB"; + } + }; + } + + private static SystemSoftwareVersionProbe softwareProbe(Map versions) { + return new SystemSoftwareVersionProbe() { + @Override + public String osVersion() { + return versions.getOrDefault("OS", ""); + } + + @Override + public String tmsVersion() { + return versions.getOrDefault("TMS", ""); + } + + @Override + public String rabbitmqVersion() { + return versions.getOrDefault("RABBITMQ", ""); + } + + @Override + public String tlqVersion() { + return versions.getOrDefault("TLQ", ""); + } + + @Override + public String tidbVersion() { + return versions.getOrDefault("TIDB", ""); + } + + @Override + public String nginxVersion() { + return versions.getOrDefault("NGINX", ""); + } + + @Override + public String standardAppVersion(String productType) { + return versions.getOrDefault("APP_" + productType, ""); + } + + @Override + public String directAppVersion() { + return versions.getOrDefault("APP_DIRECT", ""); + } + }; + } + + private static SystemHardwareProbe blankHardwareProbe() { + return new SystemHardwareProbe() { + @Override + public String cpuModel() { + return ""; + } + + @Override + public String memoryTotal() { + return ""; + } + + @Override + public String diskTotal() { + return ""; + } + }; + } + + private static MockPcieCryptoService cryptoService() { + return new MockPcieCryptoService() { + @Override + public DeviceInfoResult getDeviceInfo() { + DeviceInfoResult result = new DeviceInfoResult(); + result.setDeviceName("SYD1408加密卡"); + result.setDeviceSerial("1234567890ABCDEF"); + result.setDeviceVersion(100); + result.setStandardVersion(250); + return result; + } + }; + } + + private static DeviceSoftwareVersionEntity component(String code, String name, String version) { + DeviceSoftwareVersionEntity entity = new DeviceSoftwareVersionEntity(); + entity.setComponentCode(code); + entity.setComponentName(name); + entity.setCurrentVersion(version); + return entity; + } + + private static InitService newInitService(String productType) { + InitExecutorProperties properties = new InitExecutorProperties(); + properties.setMode(InitExecutorProperties.Mode.SIMULATE); + properties.setLogDir("/tmp/tms-init-logs"); + ObjectMapper objectMapper = new ObjectMapper(); + InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties); + return new InitService( + new InMemoryInitTaskRepository(), + new InMemoryInitTaskStepRepository(), + preset(productType), + objectMapper, + successExecutor(), + commandProfileService, + properties + ); + } + + private static CisdPresetProperties preset(String productType) { + CisdPresetProperties properties = new CisdPresetProperties(); + properties.setProductType(productType); + properties.setVersion("V1.3.2"); + properties.setSource("CONFIG"); + return properties; + } + + private static InitStepExecutor successExecutor() { + return (task, step) -> InitStepExecutionResult.success("ok", 0, null); + } + + private static InitPreviewRequest validInitRequest(String mqType) { + InitPreviewRequest request = new InitPreviewRequest(); + request.setOrgCodeType("BIC"); + request.setOrgCode("AAAABBBBXXX"); + request.setOrgNameCn("测试机构"); + request.setOrgNameEn("Test Org"); + request.setDeployMode("SINGLE"); + + InitPreviewRequest.Nodes nodes = new InitPreviewRequest.Nodes(); + nodes.setNode01Ip("10.0.0.1"); + request.setNodes(nodes); + + InitPreviewRequest.Mq mq = new InitPreviewRequest.Mq(); + mq.setMqType(mqType); + mq.setChannelUsername("bank_user"); + mq.setChannelPassword("bank_pass"); + if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { + mq.setTlqLicenseFileId("file-tlq-001"); + } + request.setMq(mq); + + InitPreviewRequest.Licenses licenses = new InitPreviewRequest.Licenses(); + licenses.setReceiverLicenseFileId("file-receiver-001"); + request.setLicenses(licenses); + return request; + } + + private static void createSuccessfulInitTask(InitService service, InitPreviewRequest request) { + InitCreateTaskResponse created = service.createTask(request); + service.executeTask(created.getTaskId()); + awaitTaskStatus(service, created.getTaskId(), "SUCCESS"); + } + + private static void awaitTaskStatus(InitService service, String taskId, String expectedStatus) { + long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(3); + while (System.nanoTime() < deadline) { + if (expectedStatus.equals(service.getTask(taskId).getStatus())) { + return; + } + try { + Thread.sleep(20L); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + Assertions.fail("interrupted while waiting for task status"); + } + } + Assertions.fail("expected task status " + expectedStatus + " within timeout"); + } + + private static class InMemoryDeviceSoftwareVersionRepository implements DeviceSoftwareVersionRepository { + + private final Map data = new HashMap<>(); + + @Override + public Optional findByComponentCode(String componentCode) { + return Optional.ofNullable(data.get(componentCode)); + } + + @Override + public void saveOrUpdate(DeviceSoftwareVersionEntity entity) { + data.put(entity.getComponentCode(), entity); + } + + void save(DeviceSoftwareVersionEntity entity) { + saveOrUpdate(entity); + } + } + + private static class InMemoryInitTaskRepository implements InitTaskRepository { + + private final Map data = new HashMap<>(); + + @Override + public Optional findByTaskId(String taskId) { + return Optional.ofNullable(data.get(taskId)); + } + + @Override + public Optional findLatest() { + return data.values().stream().max(InMemoryInitTaskRepository::compareTaskRecency); + } + + @Override + public Optional findLatestByStatus(String status) { + return data.values().stream() + .filter(entity -> status.equals(entity.getStatus())) + .max(InMemoryInitTaskRepository::compareTaskRecency); + } + + @Override + public Optional findLatestByTaskTypeAndStatus(String taskType, String status) { + return data.values().stream() + .filter(entity -> taskType.equals(entity.getTaskType())) + .filter(entity -> status.equals(entity.getStatus())) + .max(InMemoryInitTaskRepository::compareTaskRecency); + } + + @Override + public void save(InitTaskEntity entity) { + if (entity.getCreateTime() == null) { + entity.setCreateTime(LocalDateTime.now()); + } + entity.setUpdateTime(LocalDateTime.now()); + data.put(entity.getTaskId(), entity); + } + + @Override + public void update(InitTaskEntity entity) { + entity.setUpdateTime(LocalDateTime.now()); + data.put(entity.getTaskId(), entity); + } + + private static int compareTaskRecency(InitTaskEntity left, InitTaskEntity right) { + int byUpdate = Comparator.nullsFirst(LocalDateTime::compareTo).compare(left.getUpdateTime(), right.getUpdateTime()); + if (byUpdate != 0) { + return byUpdate; + } + int byCreate = Comparator.nullsFirst(LocalDateTime::compareTo).compare(left.getCreateTime(), right.getCreateTime()); + if (byCreate != 0) { + return byCreate; + } + return left.getTaskId().compareTo(right.getTaskId()); + } + } + + private static class InMemoryInitTaskStepRepository implements InitTaskStepRepository { + + private final Map> data = new ConcurrentHashMap<>(); + + @Override + public void saveBatch(List entities) { + if (entities == null || entities.isEmpty()) { + return; + } + data.put(entities.get(0).getTaskId(), new ArrayList<>(entities)); + } + + @Override + public List findByTaskIdOrderByStepNo(String taskId) { + return new ArrayList<>(data.getOrDefault(taskId, List.of())); + } + + @Override + public void updateBatch(List entities) { + if (entities == null || entities.isEmpty()) { + return; + } + data.put(entities.get(0).getTaskId(), new ArrayList<>(entities)); + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusServiceTest.java b/src/test/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusServiceTest.java new file mode 100644 index 0000000..1fa5403 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/device/service/DeviceRuntimeStatusServiceTest.java @@ -0,0 +1,510 @@ +package com.cisd.tms.modules.device.service; + +import com.cisd.tms.common.config.properties.CisdPresetProperties; +import com.cisd.tms.integration.crypto.pcie.model.DeviceInfoResult; +import com.cisd.tms.integration.crypto.pcie.service.MockPcieCryptoService; +import com.cisd.tms.modules.device.dto.DeviceRuntimeStatusResponse; +import com.cisd.tms.modules.device.dto.SoftwareRuntimeStatusItem; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.device.support.HardwareStatusProbe; +import com.cisd.tms.modules.device.support.SoftwareRuntimeProbe; +import com.cisd.tms.modules.device.support.UsageSnapshot; +import com.cisd.tms.modules.device.support.SystemSoftwareVersionProbe; +import com.cisd.tms.modules.init.config.InitCommandProfileService; +import com.cisd.tms.modules.init.config.InitExecutorProperties; +import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; +import com.cisd.tms.modules.init.dto.InitPreviewRequest; +import com.cisd.tms.modules.init.entity.InitTaskEntity; +import com.cisd.tms.modules.init.entity.InitTaskStepEntity; +import com.cisd.tms.modules.init.executor.InitStepExecutionResult; +import com.cisd.tms.modules.init.executor.InitStepExecutor; +import com.cisd.tms.modules.init.repository.InitTaskRepository; +import com.cisd.tms.modules.init.repository.InitTaskStepRepository; +import com.cisd.tms.modules.init.service.InitService; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class DeviceRuntimeStatusServiceTest { + + @Test + void shouldReturnRuntimeStatusesForEnterpriseRabbitmqInit() { + InitService initService = newInitService("ENTERPRISE"); + createSuccessfulInitTask(initService, validInitRequest("RABBITMQ")); + + InMemoryDeviceSoftwareVersionRepository versions = new InMemoryDeviceSoftwareVersionRepository(); + versions.save(component("RABBITMQ", "消息队列", "RabbitMQ V3.8.21")); + versions.save(component("TLQ", "消息队列", "tonglink/Q V8.1.11")); + versions.save(component("TIDB", "数据库", "TiDB V8.1.0")); + versions.save(component("NGINX", "Nginx", "Nginx V1.20")); + versions.save(component("APP", "标准收发器企业版", "标准收发器企业版 V1.3.2")); + + DeviceRuntimeStatusService service = new DeviceRuntimeStatusService( + initService, + cryptoService(), + hardwareProbe(23.22, 23.55, 32.0, 500.32, 1024.0), + softwareVersionProbe("SYD TMS V1.00"), + softwareRuntimeProbe(Map.of( + "RABBITMQ", true, + "TIDB", true, + "NGINX", false, + "APP_STANDARD", true, + "TMS", true + )), + versions, + preset("ENTERPRISE") + ); + + DeviceRuntimeStatusResponse response = service.loadRuntimeStatus(); + + Assertions.assertEquals(23.22, response.getHardware().getCpu().getUsagePercent()); + Assertions.assertEquals("NORMAL", response.getHardware().getCpu().getStatus()); + Assertions.assertEquals(23.55, response.getHardware().getMemory().getUsedGb()); + Assertions.assertEquals(32.0, response.getHardware().getMemory().getTotalGb()); + Assertions.assertEquals("NORMAL", response.getHardware().getCryptoCard().getStatus()); + + assertSoftwareStatus(response.getSoftware(), "RABBITMQ", "NORMAL"); + assertSoftwareStatus(response.getSoftware(), "TLQ", "NOT_INSTALLED"); + assertSoftwareStatus(response.getSoftware(), "TIDB", "NORMAL"); + assertSoftwareStatus(response.getSoftware(), "NGINX", "ABNORMAL"); + assertSoftwareStatus(response.getSoftware(), "APP", "NORMAL"); + assertSoftwareStatus(response.getSoftware(), "TMS", "NORMAL"); + } + + @Test + void shouldMarkNonInitializedSoftwareAsNotInstalled() { + DeviceRuntimeStatusService service = new DeviceRuntimeStatusService( + newInitService("ENTERPRISE"), + cryptoService(), + hardwareProbe(10.0, 8.0, 16.0, 100.0, 256.0), + softwareVersionProbe("SYD TMS V1.00"), + softwareRuntimeProbe(Map.of("TMS", true)), + new InMemoryDeviceSoftwareVersionRepository(), + preset("ENTERPRISE") + ); + + DeviceRuntimeStatusResponse response = service.loadRuntimeStatus(); + + assertSoftwareStatus(response.getSoftware(), "RABBITMQ", "NOT_INSTALLED"); + assertSoftwareStatus(response.getSoftware(), "TLQ", "NOT_INSTALLED"); + assertSoftwareStatus(response.getSoftware(), "TIDB", "NOT_INSTALLED"); + assertSoftwareStatus(response.getSoftware(), "NGINX", "NOT_INSTALLED"); + assertSoftwareStatus(response.getSoftware(), "APP", "NOT_INSTALLED"); + assertSoftwareStatus(response.getSoftware(), "TMS", "NORMAL"); + } + + @Test + void shouldReturnUnknownWhenProbeFails() { + InitService initService = newInitService("INDIRECT"); + createSuccessfulInitTask(initService, validInitRequest("RABBITMQ_TLQ")); + + DeviceRuntimeStatusService service = new DeviceRuntimeStatusService( + initService, + failingCryptoService(), + failingHardwareProbe(), + softwareVersionProbe("SYD TMS V1.00"), + failingSoftwareRuntimeProbe(), + new InMemoryDeviceSoftwareVersionRepository(), + preset("INDIRECT") + ); + + DeviceRuntimeStatusResponse response = service.loadRuntimeStatus(); + + Assertions.assertEquals("UNKNOWN", response.getHardware().getCpu().getStatus()); + Assertions.assertEquals("UNKNOWN", response.getHardware().getMemory().getStatus()); + Assertions.assertEquals("UNKNOWN", response.getHardware().getDisk().getStatus()); + Assertions.assertEquals("ABNORMAL", response.getHardware().getCryptoCard().getStatus()); + + assertSoftwareStatus(response.getSoftware(), "RABBITMQ", "UNKNOWN"); + assertSoftwareStatus(response.getSoftware(), "TLQ", "UNKNOWN"); + assertSoftwareStatus(response.getSoftware(), "TIDB", "UNKNOWN"); + assertSoftwareStatus(response.getSoftware(), "NGINX", "UNKNOWN"); + assertSoftwareStatus(response.getSoftware(), "APP", "UNKNOWN"); + assertSoftwareStatus(response.getSoftware(), "TMS", "UNKNOWN"); + } + + private static void assertSoftwareStatus(List items, String code, String expectedStatus) { + SoftwareRuntimeStatusItem item = items.stream() + .filter(candidate -> code.equals(candidate.getComponentCode())) + .findFirst() + .orElseThrow(); + Assertions.assertEquals(expectedStatus, item.getStatus()); + } + + private static HardwareStatusProbe hardwareProbe(double cpu, double memoryUsed, double memoryTotal, double diskUsed, double diskTotal) { + return new HardwareStatusProbe() { + @Override + public Double cpuUsagePercent() { + return cpu; + } + + @Override + public UsageSnapshot memoryUsage() { + return new UsageSnapshot(memoryUsed, memoryTotal); + } + + @Override + public UsageSnapshot diskUsage() { + return new UsageSnapshot(diskUsed, diskTotal); + } + }; + } + + private static HardwareStatusProbe failingHardwareProbe() { + return new HardwareStatusProbe() { + @Override + public Double cpuUsagePercent() { + throw new IllegalStateException("cpu probe failed"); + } + + @Override + public UsageSnapshot memoryUsage() { + throw new IllegalStateException("memory probe failed"); + } + + @Override + public UsageSnapshot diskUsage() { + throw new IllegalStateException("disk probe failed"); + } + }; + } + + private static SystemSoftwareVersionProbe softwareVersionProbe(String tmsVersion) { + return new SystemSoftwareVersionProbe() { + @Override + public String osVersion() { + return ""; + } + + @Override + public String tmsVersion() { + return tmsVersion; + } + + @Override + public String rabbitmqVersion() { + return ""; + } + + @Override + public String tlqVersion() { + return ""; + } + + @Override + public String tidbVersion() { + return ""; + } + + @Override + public String nginxVersion() { + return ""; + } + + @Override + public String standardAppVersion(String productType) { + return ""; + } + + @Override + public String directAppVersion() { + return ""; + } + }; + } + + private static SoftwareRuntimeProbe softwareRuntimeProbe(Map statuses) { + return new SoftwareRuntimeProbe() { + @Override + public boolean isRabbitmqRunning() { + return statuses.getOrDefault("RABBITMQ", false); + } + + @Override + public boolean isTlqRunning() { + return statuses.getOrDefault("TLQ", false); + } + + @Override + public boolean isTidbRunning() { + return statuses.getOrDefault("TIDB", false); + } + + @Override + public boolean isNginxRunning() { + return statuses.getOrDefault("NGINX", false); + } + + @Override + public boolean isStandardAppRunning() { + return statuses.getOrDefault("APP_STANDARD", false); + } + + @Override + public boolean isDirectAppRunning() { + return statuses.getOrDefault("APP_DIRECT", false); + } + + @Override + public boolean isTmsRunning() { + return statuses.getOrDefault("TMS", false); + } + }; + } + + private static SoftwareRuntimeProbe failingSoftwareRuntimeProbe() { + return new SoftwareRuntimeProbe() { + @Override + public boolean isRabbitmqRunning() { + throw new IllegalStateException("rabbitmq probe failed"); + } + + @Override + public boolean isTlqRunning() { + throw new IllegalStateException("tlq probe failed"); + } + + @Override + public boolean isTidbRunning() { + throw new IllegalStateException("tidb probe failed"); + } + + @Override + public boolean isNginxRunning() { + throw new IllegalStateException("nginx probe failed"); + } + + @Override + public boolean isStandardAppRunning() { + throw new IllegalStateException("app probe failed"); + } + + @Override + public boolean isDirectAppRunning() { + throw new IllegalStateException("app probe failed"); + } + + @Override + public boolean isTmsRunning() { + throw new IllegalStateException("tms probe failed"); + } + }; + } + + private static MockPcieCryptoService cryptoService() { + return new MockPcieCryptoService() { + @Override + public DeviceInfoResult getDeviceInfo() { + DeviceInfoResult result = new DeviceInfoResult(); + result.setDeviceName("SYD1408加密卡"); + return result; + } + }; + } + + private static MockPcieCryptoService failingCryptoService() { + return new MockPcieCryptoService() { + @Override + public DeviceInfoResult getDeviceInfo() { + throw new IllegalStateException("crypto card unavailable"); + } + }; + } + + private static DeviceSoftwareVersionEntity component(String code, String name, String version) { + DeviceSoftwareVersionEntity entity = new DeviceSoftwareVersionEntity(); + entity.setComponentCode(code); + entity.setComponentName(name); + entity.setCurrentVersion(version); + return entity; + } + + private static InitService newInitService(String productType) { + InitExecutorProperties properties = new InitExecutorProperties(); + properties.setMode(InitExecutorProperties.Mode.SIMULATE); + properties.setLogDir("/tmp/tms-init-logs"); + ObjectMapper objectMapper = new ObjectMapper(); + InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties); + return new InitService( + new InMemoryInitTaskRepository(), + new InMemoryInitTaskStepRepository(), + preset(productType), + objectMapper, + successExecutor(), + commandProfileService, + properties + ); + } + + private static CisdPresetProperties preset(String productType) { + CisdPresetProperties properties = new CisdPresetProperties(); + properties.setProductType(productType); + properties.setVersion("V1.3.2"); + properties.setSource("CONFIG"); + return properties; + } + + private static InitStepExecutor successExecutor() { + return (task, step) -> InitStepExecutionResult.success("ok", 0, null); + } + + private static InitPreviewRequest validInitRequest(String mqType) { + InitPreviewRequest request = new InitPreviewRequest(); + request.setOrgCodeType("BIC"); + request.setOrgCode("AAAABBBBXXX"); + request.setOrgNameCn("测试机构"); + request.setOrgNameEn("Test Org"); + request.setDeployMode("SINGLE"); + + InitPreviewRequest.Nodes nodes = new InitPreviewRequest.Nodes(); + nodes.setNode01Ip("10.0.0.1"); + request.setNodes(nodes); + + InitPreviewRequest.Mq mq = new InitPreviewRequest.Mq(); + mq.setMqType(mqType); + mq.setChannelUsername("bank_user"); + mq.setChannelPassword("bank_pass"); + if ("TLQ".equals(mqType) || "RABBITMQ_TLQ".equals(mqType)) { + mq.setTlqLicenseFileId("file-tlq-001"); + } + request.setMq(mq); + + InitPreviewRequest.Licenses licenses = new InitPreviewRequest.Licenses(); + licenses.setReceiverLicenseFileId("file-receiver-001"); + request.setLicenses(licenses); + return request; + } + + private static void createSuccessfulInitTask(InitService service, InitPreviewRequest request) { + InitCreateTaskResponse created = service.createTask(request); + service.executeTask(created.getTaskId()); + awaitTaskStatus(service, created.getTaskId(), "SUCCESS"); + } + + private static void awaitTaskStatus(InitService service, String taskId, String expectedStatus) { + long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(3); + while (System.nanoTime() < deadline) { + if (expectedStatus.equals(service.getTask(taskId).getStatus())) { + return; + } + try { + Thread.sleep(20L); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + Assertions.fail("interrupted while waiting for task status"); + } + } + Assertions.fail("expected task status " + expectedStatus + " within timeout"); + } + + private static class InMemoryDeviceSoftwareVersionRepository implements DeviceSoftwareVersionRepository { + + private final Map data = new HashMap<>(); + + @Override + public Optional findByComponentCode(String componentCode) { + return Optional.ofNullable(data.get(componentCode)); + } + + @Override + public void saveOrUpdate(DeviceSoftwareVersionEntity entity) { + data.put(entity.getComponentCode(), entity); + } + + void save(DeviceSoftwareVersionEntity entity) { + saveOrUpdate(entity); + } + } + + private static class InMemoryInitTaskRepository implements InitTaskRepository { + + private final Map data = new HashMap<>(); + + @Override + public Optional findByTaskId(String taskId) { + return Optional.ofNullable(data.get(taskId)); + } + + @Override + public Optional findLatest() { + return data.values().stream().max(InMemoryInitTaskRepository::compareTaskRecency); + } + + @Override + public Optional findLatestByStatus(String status) { + return data.values().stream() + .filter(entity -> status.equals(entity.getStatus())) + .max(InMemoryInitTaskRepository::compareTaskRecency); + } + + @Override + public Optional findLatestByTaskTypeAndStatus(String taskType, String status) { + return data.values().stream() + .filter(entity -> taskType.equals(entity.getTaskType())) + .filter(entity -> status.equals(entity.getStatus())) + .max(InMemoryInitTaskRepository::compareTaskRecency); + } + + @Override + public void save(InitTaskEntity entity) { + if (entity.getCreateTime() == null) { + entity.setCreateTime(LocalDateTime.now()); + } + entity.setUpdateTime(LocalDateTime.now()); + data.put(entity.getTaskId(), entity); + } + + @Override + public void update(InitTaskEntity entity) { + entity.setUpdateTime(LocalDateTime.now()); + data.put(entity.getTaskId(), entity); + } + + private static int compareTaskRecency(InitTaskEntity left, InitTaskEntity right) { + int byUpdate = Comparator.nullsFirst(LocalDateTime::compareTo).compare(left.getUpdateTime(), right.getUpdateTime()); + if (byUpdate != 0) { + return byUpdate; + } + int byCreate = Comparator.nullsFirst(LocalDateTime::compareTo).compare(left.getCreateTime(), right.getCreateTime()); + if (byCreate != 0) { + return byCreate; + } + return left.getTaskId().compareTo(right.getTaskId()); + } + } + + private static class InMemoryInitTaskStepRepository implements InitTaskStepRepository { + + private final Map> data = new ConcurrentHashMap<>(); + + @Override + public void saveBatch(List entities) { + if (entities == null || entities.isEmpty()) { + return; + } + data.put(entities.get(0).getTaskId(), new ArrayList<>(entities)); + } + + @Override + public List findByTaskIdOrderByStepNo(String taskId) { + return new ArrayList<>(data.getOrDefault(taskId, List.of())); + } + + @Override + public void updateBatch(List entities) { + if (entities == null || entities.isEmpty()) { + return; + } + data.put(entities.get(0).getTaskId(), new ArrayList<>(entities)); + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/device/service/DeviceServiceTest.java b/src/test/java/com/cisd/tms/modules/device/service/DeviceServiceTest.java new file mode 100644 index 0000000..fead408 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/device/service/DeviceServiceTest.java @@ -0,0 +1,227 @@ +package com.cisd.tms.modules.device.service; + +import com.cisd.tms.common.config.properties.CisdPresetProperties; +import com.cisd.tms.modules.device.dto.DeviceInfoResponse; +import com.cisd.tms.modules.device.entity.DeviceNodeEntity; +import com.cisd.tms.modules.device.repository.DeviceNodeRepository; +import com.cisd.tms.modules.init.config.InitCommandProfileService; +import com.cisd.tms.modules.init.config.InitExecutorProperties; +import com.cisd.tms.modules.init.dto.InitCreateTaskResponse; +import com.cisd.tms.modules.init.dto.InitPreviewRequest; +import com.cisd.tms.modules.init.entity.InitTaskEntity; +import com.cisd.tms.modules.init.entity.InitTaskStepEntity; +import com.cisd.tms.modules.init.executor.InitStepExecutionResult; +import com.cisd.tms.modules.init.executor.InitStepExecutor; +import com.cisd.tms.modules.init.repository.InitTaskRepository; +import com.cisd.tms.modules.init.repository.InitTaskStepRepository; +import com.cisd.tms.modules.init.service.InitService; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class DeviceServiceTest { + + @Test + void shouldReportUninitializedWhenNoSuccessfulInitTask() { + DeviceService service = newDeviceService("ENTERPRISE"); + + DeviceInfoResponse response = service.info(); + + Assertions.assertEquals("UNINITIALIZED", response.getInitState()); + } + + @Test + void shouldReportInitializedContextFromLatestSuccessfulInitTask() { + InMemoryInitTaskRepository taskRepository = new InMemoryInitTaskRepository(); + InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); + InitService initService = newInitService(taskRepository, stepRepository, "ENTERPRISE"); + DeviceService deviceService = new DeviceService(new FixedDeviceNodeRepository(), preset("ENTERPRISE"), initService); + + InitCreateTaskResponse created = initService.createTask(validEnterpriseRequest()); + initService.executeTask(created.getTaskId()); + awaitTaskStatus(initService, created.getTaskId(), "SUCCESS"); + + DeviceInfoResponse response = deviceService.info(); + + Assertions.assertEquals("INITIALIZED", response.getInitState()); + } + + private static DeviceService newDeviceService(String productType) { + InMemoryInitTaskRepository taskRepository = new InMemoryInitTaskRepository(); + InMemoryInitTaskStepRepository stepRepository = new InMemoryInitTaskStepRepository(); + InitService initService = newInitService(taskRepository, stepRepository, productType); + return new DeviceService(new FixedDeviceNodeRepository(), preset(productType), initService); + } + + private static InitService newInitService( + InitTaskRepository taskRepository, + InitTaskStepRepository stepRepository, + String productType + ) { + InitExecutorProperties properties = new InitExecutorProperties(); + properties.setMode(InitExecutorProperties.Mode.SIMULATE); + properties.setLogDir("/tmp/tms-init-logs"); + ObjectMapper objectMapper = new ObjectMapper(); + InitCommandProfileService commandProfileService = new InitCommandProfileService(objectMapper, properties); + return new InitService( + taskRepository, + stepRepository, + preset(productType), + objectMapper, + successExecutor(), + commandProfileService, + properties + ); + } + + private static CisdPresetProperties preset(String productType) { + CisdPresetProperties properties = new CisdPresetProperties(); + properties.setProductType(productType); + properties.setVersion("V1.3.2"); + properties.setSource("CONFIG"); + return properties; + } + + private static InitStepExecutor successExecutor() { + return (task, step) -> InitStepExecutionResult.success("ok", 0, null); + } + + private static InitPreviewRequest validEnterpriseRequest() { + InitPreviewRequest request = new InitPreviewRequest(); + request.setOrgCodeType("BIC"); + request.setOrgCode("AAAABBBBXXX"); + request.setOrgNameCn("测试机构"); + request.setDeployMode("SINGLE"); + + InitPreviewRequest.Nodes nodes = new InitPreviewRequest.Nodes(); + nodes.setNode01Ip("10.0.0.1"); + request.setNodes(nodes); + + InitPreviewRequest.Mq mq = new InitPreviewRequest.Mq(); + mq.setMqType("RABBITMQ"); + mq.setChannelUsername("bank_user"); + mq.setChannelPassword("bank_pass"); + request.setMq(mq); + + InitPreviewRequest.Licenses licenses = new InitPreviewRequest.Licenses(); + licenses.setReceiverLicenseFileId("file-receiver-001"); + request.setLicenses(licenses); + return request; + } + + private static void awaitTaskStatus(InitService service, String taskId, String expectedStatus) { + long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(3); + while (System.nanoTime() < deadline) { + if (expectedStatus.equals(service.getTask(taskId).getStatus())) { + return; + } + try { + Thread.sleep(20L); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + Assertions.fail("interrupted while waiting for task status"); + } + } + Assertions.fail("expected task status " + expectedStatus + " within timeout"); + } + + private static class FixedDeviceNodeRepository implements DeviceNodeRepository { + @Override + public Optional findByNodeId(String nodeId) { + DeviceNodeEntity entity = new DeviceNodeEntity(); + entity.setNodeId(nodeId); + entity.setStatus("RUNNING"); + return Optional.of(entity); + } + } + + private static class InMemoryInitTaskRepository implements InitTaskRepository { + + private final Map data = new HashMap<>(); + + @Override + public Optional findByTaskId(String taskId) { + return Optional.ofNullable(data.get(taskId)); + } + + @Override + public Optional findLatest() { + return data.values().stream().max(InMemoryInitTaskRepository::compareTaskRecency); + } + + @Override + public Optional findLatestByStatus(String status) { + return data.values().stream() + .filter(entity -> status.equals(entity.getStatus())) + .max(InMemoryInitTaskRepository::compareTaskRecency); + } + + @Override + public Optional findLatestByTaskTypeAndStatus(String taskType, String status) { + return data.values().stream() + .filter(entity -> taskType.equals(entity.getTaskType())) + .filter(entity -> status.equals(entity.getStatus())) + .max(InMemoryInitTaskRepository::compareTaskRecency); + } + + @Override + public void save(InitTaskEntity entity) { + if (entity.getCreateTime() == null) { + entity.setCreateTime(LocalDateTime.now()); + } + entity.setUpdateTime(LocalDateTime.now()); + data.put(entity.getTaskId(), entity); + } + + @Override + public void update(InitTaskEntity entity) { + entity.setUpdateTime(LocalDateTime.now()); + data.put(entity.getTaskId(), entity); + } + + private static int compareTaskRecency(InitTaskEntity left, InitTaskEntity right) { + int byUpdate = java.util.Comparator.nullsFirst(LocalDateTime::compareTo).compare(left.getUpdateTime(), right.getUpdateTime()); + if (byUpdate != 0) { + return byUpdate; + } + int byCreate = java.util.Comparator.nullsFirst(LocalDateTime::compareTo).compare(left.getCreateTime(), right.getCreateTime()); + if (byCreate != 0) { + return byCreate; + } + return left.getTaskId().compareTo(right.getTaskId()); + } + } + + private static class InMemoryInitTaskStepRepository implements InitTaskStepRepository { + + private final Map> data = new ConcurrentHashMap<>(); + + @Override + public void saveBatch(List entities) { + if (entities == null || entities.isEmpty()) { + return; + } + data.put(entities.get(0).getTaskId(), new ArrayList<>(entities)); + } + + @Override + public List findByTaskIdOrderByStepNo(String taskId) { + return new ArrayList<>(data.getOrDefault(taskId, List.of())); + } + + @Override + public void updateBatch(List entities) { + if (entities == null || entities.isEmpty()) { + return; + } + data.put(entities.get(0).getTaskId(), new ArrayList<>(entities)); + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/mk/controller/LmkControllerTest.java b/src/test/java/com/cisd/tms/modules/mk/controller/LmkControllerTest.java new file mode 100644 index 0000000..5ae9cc8 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/mk/controller/LmkControllerTest.java @@ -0,0 +1,130 @@ +package com.cisd.tms.modules.mk.controller; + +import com.cisd.tms.common.exception.GlobalExceptionHandler; +import com.cisd.tms.modules.mk.dto.MasterKeyBackupMaterial; +import com.cisd.tms.modules.mk.dto.MasterKeyStateResult; +import com.cisd.tms.modules.mk.service.LmkService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.eq; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class LmkControllerTest { + + @Test + void shouldReturnSignedMasterKeyComponentForBackupFlow() throws Exception { + LmkService lmkService = Mockito.mock(LmkService.class); + Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001"); + Mockito.when(lmkService.signIk(ArgumentMatchers.anyString())).thenReturn("SIGN-001"); + Mockito.when(lmkService.getBackupMaterial(2)) + .thenReturn(new MasterKeyBackupMaterial("COMP-002", "LMK-MAC-001")); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new LmkController(lmkService, new ObjectMapper())) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/masterKey/backup") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "pubKey": "PUB-001", + "role": "superadmin", + "uid": "2", + "rid": "RID-001", + "extra": "ignored-extra" + } + """)) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"sign\":\"SIGN-001\""))) + .andExpect(content().string(containsString("\"component\":\"COMP-002\""))) + .andExpect(content().string(containsString("\"lmkMac\":\"LMK-MAC-001\""))); + + Mockito.verify(lmkService).signIk(eq("{\"pubKey\":\"PUB-001\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"superadmin\",\"uid\":\"2\",\"rid\":\"RID-001\"}")); + Mockito.verify(lmkService).getBackupMaterial(2); + } + + @Test + void shouldRecoverLmkExUsingLmkMacAndOrderedComponents() throws Exception { + LmkService lmkService = Mockito.mock(LmkService.class); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new LmkController(lmkService, new ObjectMapper())) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/masterKey/recover") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "lmkMac": "a1a2a3a4a5a6a7a8", + "components": [ + "0102", + "0304", + "0506" + ] + } + """)) + .andExpect(status().isOk()); + + Mockito.verify(lmkService).recoverLmkEx(eq("a1a2a3a4a5a6a7a8"), eq(java.util.List.of("0102", "0304", "0506"))); + } + + @Test + void shouldInitMasterKeyThroughNewLifecycleEndpoint() throws Exception { + LmkService lmkService = Mockito.mock(LmkService.class); + MasterKeyStateResult result = new MasterKeyStateResult(); + result.setStatus(true); + result.setSeedMac("a1b2c3d4"); + Mockito.when(lmkService.initMasterKey()).thenReturn(result); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new LmkController(lmkService, new ObjectMapper())) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/masterKey/init")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"status\":true"))) + .andExpect(content().string(containsString("\"seedMac\":\"a1b2c3d4\""))); + } + + @Test + void shouldDestroyMasterKeyThroughNewLifecycleEndpoint() throws Exception { + LmkService lmkService = Mockito.mock(LmkService.class); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new LmkController(lmkService, new ObjectMapper())) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(post("/api/v1/masterKey/destroy")) + .andExpect(status().isOk()); + + Mockito.verify(lmkService).destroyMasterKey(); + } + + @Test + void shouldReturnMasterKeyLifecycleStatus() throws Exception { + LmkService lmkService = Mockito.mock(LmkService.class); + MasterKeyStateResult result = new MasterKeyStateResult(); + result.setStatus(true); + result.setSeedMac("00112233"); + Mockito.when(lmkService.queryMasterKeyStatus()).thenReturn(result); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new LmkController(lmkService, new ObjectMapper())) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + + mockMvc.perform(get("/api/v1/masterKey/status")) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("\"status\":true"))) + .andExpect(content().string(containsString("\"seedMac\":\"00112233\""))); + } +} diff --git a/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java b/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java new file mode 100644 index 0000000..bddbe5b --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/mk/service/LmkServiceTest.java @@ -0,0 +1,132 @@ +package com.cisd.tms.modules.mk.service; + +import com.cisd.tms.integration.crypto.pcie.model.BackupDataResult; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.mk.config.MasterKeyInitProperties; +import com.cisd.tms.modules.mk.dto.MasterKeyBackupMaterial; +import com.cisd.tms.modules.mk.dto.MasterKeyStateResult; +import java.util.List; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +class LmkServiceTest { + + @Test + void shouldInitMasterKeyWithConfiguredPinsAndReturnSeedMac() { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE="); + Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any())).thenReturn(new byte[] {0x01, 0x23, 0x45, 0x67}); + + LmkService service = new LmkService(pcieCryptoService, properties); + + MasterKeyStateResult result = service.initMasterKey(); + + Assertions.assertTrue(result.isStatus()); + Assertions.assertEquals("01234567", result.getSeedMac()); + ArgumentCaptor oldPinCaptor = ArgumentCaptor.forClass(byte[].class); + ArgumentCaptor newPinCaptor = ArgumentCaptor.forClass(byte[].class); + Mockito.verify(pcieCryptoService).initializeMasterKey(oldPinCaptor.capture(), newPinCaptor.capture()); + Assertions.assertArrayEquals("12345678".getBytes(StandardCharsets.UTF_8), oldPinCaptor.getValue()); + Assertions.assertArrayEquals("87654321".getBytes(StandardCharsets.UTF_8), newPinCaptor.getValue()); + } + + @Test + void shouldPropagateInitMasterKeyFailure() { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE="); + Mockito.when(pcieCryptoService.initializeMasterKey(Mockito.any(), Mockito.any())) + .thenThrow(new IllegalStateException("master key initialization failed")); + + LmkService service = new LmkService(pcieCryptoService, properties); + + IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class, service::initMasterKey); + + Assertions.assertEquals("master key initialization failed", exception.getMessage()); + } + + @Test + void shouldReturnStatusFalseWhenMasterKeyDoesNotExist() { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE="); + Mockito.when(pcieCryptoService.checkLmk()).thenReturn(false); + + LmkService service = new LmkService(pcieCryptoService, properties); + + MasterKeyStateResult result = service.queryMasterKeyStatus(); + + Assertions.assertFalse(result.isStatus()); + Assertions.assertNull(result.getSeedMac()); + Mockito.verify(pcieCryptoService, Mockito.never()).exportLmkSeedMac(); + } + + @Test + void shouldReturnStatusTrueAndSeedMacWhenMasterKeyExists() { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE="); + Mockito.when(pcieCryptoService.checkLmk()).thenReturn(true); + Mockito.when(pcieCryptoService.exportLmkSeedMac()).thenReturn(new byte[] {(byte) 0xAA, (byte) 0xBB}); + + LmkService service = new LmkService(pcieCryptoService, properties); + + MasterKeyStateResult result = service.queryMasterKeyStatus(); + + Assertions.assertTrue(result.isStatus()); + Assertions.assertEquals("aabb", result.getSeedMac()); + } + + @Test + void shouldReturnIndexedMasterKeyComponentAndMacFromBackupBlob() { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE="); + BackupDataResult backup = new BackupDataResult(); + backup.setLength(20); + backup.setData(new byte[] { + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0A, 0x0B, 0x0C + }); + Mockito.when(pcieCryptoService.backupLmkEx()).thenReturn(backup); + + LmkService service = new LmkService(pcieCryptoService, properties); + + MasterKeyBackupMaterial material = service.getBackupMaterial(2); + + Assertions.assertEquals("05060708", material.getComponent()); + Assertions.assertEquals("1112131415161718", material.getLmkMac()); + } + + @Test + void shouldInitIdentifyBeforeRecoverLmkExAndLoadIt() { + PcieCryptoService pcieCryptoService = Mockito.mock(PcieCryptoService.class); + MasterKeyInitProperties properties = configuredPins("MTIzNDU2Nzg=", "ODc2NTQzMjE="); + LmkService service = new LmkService(pcieCryptoService, properties); + + service.recoverLmkEx("1112131415161718", List.of("0102", "0304", "0506")); + + ArgumentCaptor oldPinCaptor = ArgumentCaptor.forClass(byte[].class); + ArgumentCaptor newPinCaptor = ArgumentCaptor.forClass(byte[].class); + ArgumentCaptor captor = ArgumentCaptor.forClass(byte[].class); + InOrder inOrder = Mockito.inOrder(pcieCryptoService); + inOrder.verify(pcieCryptoService).initIdentify(oldPinCaptor.capture(), newPinCaptor.capture()); + inOrder.verify(pcieCryptoService).recoverLmkEx(captor.capture()); + inOrder.verify(pcieCryptoService).loadLmk(); + Assertions.assertArrayEquals("12345678".getBytes(StandardCharsets.UTF_8), oldPinCaptor.getValue()); + Assertions.assertArrayEquals("87654321".getBytes(StandardCharsets.UTF_8), newPinCaptor.getValue()); + Assertions.assertArrayEquals(new byte[] { + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 + }, captor.getValue()); + } + + private static MasterKeyInitProperties configuredPins(String oldPinBase64, String newPinBase64) { + MasterKeyInitProperties properties = new MasterKeyInitProperties(); + properties.setOldPinBase64(oldPinBase64); + properties.setNewPinBase64(newPinBase64); + return properties; + } +} diff --git a/src/test/java/com/cisd/tms/modules/upgrade/UpgradeDtoContractTest.java b/src/test/java/com/cisd/tms/modules/upgrade/UpgradeDtoContractTest.java new file mode 100644 index 0000000..2f44518 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/upgrade/UpgradeDtoContractTest.java @@ -0,0 +1,56 @@ +package com.cisd.tms.modules.upgrade; + +import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest; +import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse; +import com.cisd.tms.modules.upgrade.enums.UpgradeTargetComponent; +import com.cisd.tms.modules.upgrade.enums.UpgradeTaskStatus; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class UpgradeDtoContractTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void shouldSerializeUpgradePreviewFields() throws Exception { + UpgradePreviewResponse response = new UpgradePreviewResponse(); + response.setTargetComponent("STANDARD_APP"); + response.setCurrentVersion("V1.3.1"); + response.setTargetVersion("V1.3.2"); + response.setVersionChangeSummary("标准收发器企业版将从 V1.3.1 升级到 V1.3.2"); + response.setVerifyPassed(true); + response.setVerifyMessage("校验通过"); + response.setAllowed(true); + response.setBlockReason(""); + response.setDescription("修复标准收发器已知问题"); + + String json = objectMapper.writeValueAsString(response); + + Assertions.assertTrue(json.contains("\"targetComponent\":\"STANDARD_APP\"")); + Assertions.assertTrue(json.contains("\"currentVersion\":\"V1.3.1\"")); + Assertions.assertTrue(json.contains("\"targetVersion\":\"V1.3.2\"")); + Assertions.assertTrue(json.contains("\"versionChangeSummary\"")); + Assertions.assertTrue(json.contains("\"verifyPassed\":true")); + Assertions.assertTrue(json.contains("\"allowed\":true")); + } + + @Test + void shouldAcceptFileIdForCreateTaskRequest() { + UpgradeCreateTaskRequest request = new UpgradeCreateTaskRequest(); + request.setFileId("FILE-20260319-000001"); + + Assertions.assertEquals("FILE-20260319-000001", request.getFileId()); + } + + @Test + void shouldExposeExpectedUpgradeEnums() { + Assertions.assertEquals("TMS", UpgradeTargetComponent.TMS.name()); + Assertions.assertEquals("STANDARD_APP", UpgradeTargetComponent.STANDARD_APP.name()); + Assertions.assertEquals("DIRECT_APP", UpgradeTargetComponent.DIRECT_APP.name()); + Assertions.assertEquals("PENDING_CONFIRM", UpgradeTaskStatus.PENDING_CONFIRM.name()); + Assertions.assertEquals("RUNNING", UpgradeTaskStatus.RUNNING.name()); + Assertions.assertEquals("SUCCESS", UpgradeTaskStatus.SUCCESS.name()); + Assertions.assertEquals("FAILED", UpgradeTaskStatus.FAILED.name()); + } +} diff --git a/src/test/java/com/cisd/tms/modules/upgrade/controller/UpgradeControllerTest.java b/src/test/java/com/cisd/tms/modules/upgrade/controller/UpgradeControllerTest.java new file mode 100644 index 0000000..4692e29 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/upgrade/controller/UpgradeControllerTest.java @@ -0,0 +1,107 @@ +package com.cisd.tms.modules.upgrade.controller; + +import com.cisd.tms.modules.file.dto.FileUploadResponse; +import com.cisd.tms.modules.file.service.FileService; +import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradeTaskPageResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradeTaskResponse; +import com.cisd.tms.modules.upgrade.service.UpgradeService; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class UpgradeControllerTest { + + @Test + void shouldUploadUpgradePackage() throws Exception { + UpgradeService upgradeService = Mockito.mock(UpgradeService.class); + FileService fileService = Mockito.mock(FileService.class); + FileUploadResponse response = new FileUploadResponse(); + response.setFileId("FILE-20260319-000001"); + response.setSize(128L); + response.setStoragePath("/home/tms/uploads/FILE-20260319-000001/upgrade-package.zip"); + Mockito.when(fileService.upload(Mockito.any())).thenReturn(response); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UpgradeController(upgradeService, fileService)).build(); + + mockMvc.perform(multipart("/api/v1/upgrade-packages") + .file(new MockMultipartFile("file", "upgrade-package.zip", "application/zip", "zip".getBytes()))) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"fileId\":\"FILE-20260319-000001\""))); + } + + @Test + void shouldPreviewCreateExecuteAndQueryUpgradeTasks() throws Exception { + UpgradeService upgradeService = Mockito.mock(UpgradeService.class); + FileService fileService = Mockito.mock(FileService.class); + + UpgradePreviewResponse preview = new UpgradePreviewResponse(); + preview.setTargetComponent("STANDARD_APP"); + preview.setCurrentVersion("V1.3.1"); + preview.setTargetVersion("V1.3.2"); + preview.setAllowed(true); + preview.setVerifyPassed(true); + Mockito.when(upgradeService.preview(Mockito.any())).thenReturn(preview); + + UpgradeCreateTaskResponse create = new UpgradeCreateTaskResponse(); + create.setTaskId("UPG-001"); + create.setStatus("PENDING_CONFIRM"); + create.setTargetComponent("STANDARD_APP"); + create.setCurrentVersion("V1.3.1"); + create.setTargetVersion("V1.3.2"); + Mockito.when(upgradeService.createTask(Mockito.any())).thenReturn(create); + + UpgradeTaskResponse task = new UpgradeTaskResponse(); + task.setTaskId("UPG-001"); + task.setStatus("RUNNING"); + Mockito.when(upgradeService.executeTask("UPG-001")).thenReturn(task); + Mockito.when(upgradeService.getTask("UPG-001")).thenReturn(task); + + UpgradeTaskPageResponse page = new UpgradeTaskPageResponse(); + page.setPage(1); + page.setPageSize(10); + page.setTotal(1L); + page.getItems().add(task); + Mockito.when(upgradeService.listTasks(1, 10, "", "")).thenReturn(page); + + MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new UpgradeController(upgradeService, fileService)).build(); + + mockMvc.perform(post("/api/v1/upgrades/previews") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"fileId\":\"FILE-001\"}")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"targetComponent\":\"STANDARD_APP\""))); + + mockMvc.perform(post("/api/v1/upgrades") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"fileId\":\"FILE-001\"}")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskId\":\"UPG-001\""))); + + mockMvc.perform(post("/api/v1/upgrades/UPG-001/execute")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"status\":\"RUNNING\""))); + + mockMvc.perform(get("/api/v1/upgrades") + .param("page", "1") + .param("pageSize", "10") + .param("targetComponent", "") + .param("status", "")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"total\":1"))); + + mockMvc.perform(get("/api/v1/upgrades/UPG-001")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("\"taskId\":\"UPG-001\""))); + } +} diff --git a/src/test/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepositoryTest.java b/src/test/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepositoryTest.java new file mode 100644 index 0000000..a2fbb59 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/upgrade/repository/UpgradeTaskRepositoryTest.java @@ -0,0 +1,104 @@ +package com.cisd.tms.modules.upgrade.repository; + +import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity; +import com.cisd.tms.modules.upgrade.mapper.UpgradeTaskMapper; +import com.cisd.tms.modules.upgrade.repository.impl.UpgradeTaskRepositoryImpl; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class UpgradeTaskRepositoryTest { + + @Test + void shouldSaveAndQueryUpgradeTask() { + UpgradeTaskMapper mapper = Mockito.mock(UpgradeTaskMapper.class); + UpgradeTaskRepository repository = new UpgradeTaskRepositoryImpl(mapper); + UpgradeTaskEntity entity = entity("UPG-001", "PENDING_CONFIRM"); + + repository.save(entity); + Mockito.verify(mapper).insert(entity); + + Mockito.when(mapper.selectByTaskId("UPG-001")).thenReturn(entity); + + UpgradeTaskEntity found = repository.findByTaskId("UPG-001").orElseThrow(); + + Assertions.assertEquals("UPG-001", found.getTaskId()); + Assertions.assertEquals("STANDARD_APP", found.getTargetComponent()); + } + + @Test + void shouldReturnRunningTaskWhenExists() { + UpgradeTaskMapper mapper = Mockito.mock(UpgradeTaskMapper.class); + UpgradeTaskRepository repository = new UpgradeTaskRepositoryImpl(mapper); + UpgradeTaskEntity entity = entity("UPG-002", "RUNNING"); + Mockito.when(mapper.selectRunningTask()).thenReturn(entity); + + UpgradeTaskEntity running = repository.findRunningTask().orElseThrow(); + + Assertions.assertEquals("RUNNING", running.getStatus()); + Assertions.assertEquals("UPG-002", running.getTaskId()); + } + + @Test + void shouldQueryPagedTasksByComponentAndStatus() { + UpgradeTaskMapper mapper = Mockito.mock(UpgradeTaskMapper.class); + UpgradeTaskRepository repository = new UpgradeTaskRepositoryImpl(mapper); + UpgradeTaskEntity entity = entity("UPG-003", "SUCCESS"); + + Mockito.when(mapper.selectPage("TMS", "SUCCESS", 0, 10)).thenReturn(List.of(entity)); + Mockito.when(mapper.countPage("TMS", "SUCCESS")).thenReturn(1L); + + List page = repository.findPage("TMS", "SUCCESS", 0, 10); + long total = repository.countPage("TMS", "SUCCESS"); + + Assertions.assertEquals(1, page.size()); + Assertions.assertEquals("UPG-003", page.get(0).getTaskId()); + Assertions.assertEquals(1L, total); + } + + @Test + void shouldUpdateTaskStatusAndLogMetadata() { + UpgradeTaskMapper mapper = Mockito.mock(UpgradeTaskMapper.class); + UpgradeTaskRepository repository = new UpgradeTaskRepositoryImpl(mapper); + LocalDateTime startedAt = LocalDateTime.of(2026, 3, 19, 10, 0, 0); + LocalDateTime finishedAt = LocalDateTime.of(2026, 3, 19, 10, 5, 0); + + repository.updateStatus( + "UPG-004", + "FAILED", + "orchestrator exited with 1", + "/home/tms/tmp/tms-upgrade-logs/UPG-004.log", + startedAt, + finishedAt + ); + + Mockito.verify(mapper).updateStatusByTaskId( + "UPG-004", + "FAILED", + "orchestrator exited with 1", + "/home/tms/tmp/tms-upgrade-logs/UPG-004.log", + startedAt, + finishedAt + ); + } + + private static UpgradeTaskEntity entity(String taskId, String status) { + UpgradeTaskEntity entity = new UpgradeTaskEntity(); + entity.setTaskId(taskId); + entity.setUpgradeMode("OFFLINE"); + entity.setTargetComponent("STANDARD_APP"); + entity.setPackageFileId("FILE-001"); + entity.setPackageName("upgrade-package.zip"); + entity.setPackageHash("abc123"); + entity.setCurrentVersion("V1.3.1"); + entity.setTargetVersion("V1.3.2"); + entity.setStatus(status); + entity.setVerifyPassed(Boolean.TRUE); + entity.setVerifyMessage("ok"); + entity.setManifestJson("{\"targetComponent\":\"STANDARD_APP\"}"); + entity.setOperator("admin"); + return entity; + } +} diff --git a/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java b/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java new file mode 100644 index 0000000..be96441 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradePackageServiceTest.java @@ -0,0 +1,255 @@ +package com.cisd.tms.modules.upgrade.service; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.device.entity.DeviceSoftwareVersionEntity; +import com.cisd.tms.modules.device.repository.DeviceSoftwareVersionRepository; +import com.cisd.tms.modules.file.entity.FileRecordEntity; +import com.cisd.tms.modules.file.repository.FileRecordRepository; +import com.cisd.tms.modules.upgrade.config.UpgradeProperties; +import com.cisd.tms.modules.upgrade.dto.UpgradeManifest; +import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse; +import com.cisd.tms.modules.upgrade.support.UpgradePackageSignatureVerifier; +import com.cisd.tms.modules.upgrade.support.UpgradePackageStagingService; +import com.cisd.tms.modules.upgrade.support.UpgradeVersionComparator; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.Map; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class UpgradePackageServiceTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + void shouldPreviewUpgradePackageWithVersionChange() throws Exception { + Path tempDir = Files.createTempDirectory("upgrade-preview-test"); + FileRecordEntity fileRecord = buildPackage(tempDir, "STANDARD_APP", "V1.3.2", "V1.3.0", "修复标准收发器问题"); + InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository(); + fileRepository.save(fileRecord); + InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository(); + versionRepository.save(component("STANDARD_APP", "标准收发器企业版", "V1.3.1")); + + UpgradePackageService service = new UpgradePackageService( + fileRepository, + versionRepository, + stagingService(tempDir.resolve("staging")), + new PassThroughSignatureVerifier(), + new UpgradeVersionComparator(), + objectMapper + ); + + UpgradePreviewResponse response = service.preview(fileRecord.getFileId()); + + Assertions.assertEquals("STANDARD_APP", response.getTargetComponent()); + Assertions.assertEquals("V1.3.1", response.getCurrentVersion()); + Assertions.assertEquals("V1.3.2", response.getTargetVersion()); + Assertions.assertTrue(response.isVerifyPassed()); + Assertions.assertTrue(response.isAllowed()); + Assertions.assertTrue(response.getVersionChangeSummary().contains("V1.3.1")); + Assertions.assertTrue(response.getVersionChangeSummary().contains("V1.3.2")); + } + + @Test + void shouldRejectPayloadChecksumMismatch() throws Exception { + Path tempDir = Files.createTempDirectory("upgrade-preview-test"); + FileRecordEntity fileRecord = buildPackage(tempDir, "TMS", "V1.0.1", "V1.0.0", "修复 TMS 问题"); + InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository(); + fileRepository.save(fileRecord); + + tamperManifestChecksum(Path.of(fileRecord.getStoragePath())); + + UpgradePackageService service = new UpgradePackageService( + fileRepository, + new InMemoryDeviceSoftwareVersionRepository(), + stagingService(tempDir.resolve("staging")), + new PassThroughSignatureVerifier(), + new UpgradeVersionComparator(), + objectMapper + ); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.preview(fileRecord.getFileId())); + Assertions.assertEquals(ErrorCode.BIZ_ERROR.getCode(), exception.getCode()); + Assertions.assertEquals("payload checksum mismatch", exception.getMessage()); + } + + @Test + void shouldRejectDowngrade() throws Exception { + Path tempDir = Files.createTempDirectory("upgrade-preview-test"); + FileRecordEntity fileRecord = buildPackage(tempDir, "TMS", "V1.0.0", "V0.9.0", "尝试降级"); + InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository(); + fileRepository.save(fileRecord); + InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository(); + versionRepository.save(component("TMS", "设备管理软件", "V1.0.1")); + + UpgradePackageService service = new UpgradePackageService( + fileRepository, + versionRepository, + stagingService(tempDir.resolve("staging")), + new PassThroughSignatureVerifier(), + new UpgradeVersionComparator(), + objectMapper + ); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.preview(fileRecord.getFileId())); + Assertions.assertEquals("target version is lower than current version", exception.getMessage()); + } + + @Test + void shouldRejectWhenCurrentVersionIsLowerThanMinCompatibleVersion() throws Exception { + Path tempDir = Files.createTempDirectory("upgrade-preview-test"); + FileRecordEntity fileRecord = buildPackage(tempDir, "DIRECT_APP", "V2.0.0", "V1.5.0", "需要更高起始版本"); + InMemoryFileRecordRepository fileRepository = new InMemoryFileRecordRepository(); + fileRepository.save(fileRecord); + InMemoryDeviceSoftwareVersionRepository versionRepository = new InMemoryDeviceSoftwareVersionRepository(); + versionRepository.save(component("DIRECT_APP", "直参轻量化终端", "V1.4.9")); + + UpgradePackageService service = new UpgradePackageService( + fileRepository, + versionRepository, + stagingService(tempDir.resolve("staging")), + new PassThroughSignatureVerifier(), + new UpgradeVersionComparator(), + objectMapper + ); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.preview(fileRecord.getFileId())); + Assertions.assertEquals("current version does not satisfy minimum compatible version", exception.getMessage()); + } + + private UpgradePackageStagingService stagingService(Path stagingRoot) { + UpgradeProperties properties = new UpgradeProperties(); + properties.setStagingRootDir(stagingRoot.toString()); + return new UpgradePackageStagingService(properties); + } + + private FileRecordEntity buildPackage(Path tempDir, String targetComponent, String targetVersion, String minCompatibleVersion, String description) + throws IOException, NoSuchAlgorithmException { + Path packagePath = tempDir.resolve("upgrade-package.zip"); + Path payloadPath = tempDir.resolve("payload.zip"); + Files.write(payloadPath, "payload-content".getBytes(StandardCharsets.UTF_8)); + + UpgradeManifest manifest = new UpgradeManifest(); + manifest.setPackageId("PKG-001"); + manifest.setTargetComponent(targetComponent); + manifest.setTargetVersion(targetVersion); + manifest.setMinCompatibleVersion(minCompatibleVersion); + manifest.setDescription(description); + manifest.setOrchestrator("scripts/orchestrator.sh"); + UpgradeManifest.FileChecksums checksums = new UpgradeManifest.FileChecksums(); + checksums.setPayloadZip(sha256Hex(payloadPath)); + manifest.setFileChecksums(checksums); + + byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest); + byte[] signatureBytes = "signature".getBytes(StandardCharsets.UTF_8); + + try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(packagePath))) { + outputStream.putNextEntry(new ZipEntry("manifest.json")); + outputStream.write(manifestBytes); + outputStream.closeEntry(); + + outputStream.putNextEntry(new ZipEntry("signature.sig")); + outputStream.write(signatureBytes); + outputStream.closeEntry(); + + outputStream.putNextEntry(new ZipEntry("payload.zip")); + outputStream.write(Files.readAllBytes(payloadPath)); + outputStream.closeEntry(); + } + + FileRecordEntity entity = new FileRecordEntity(); + entity.setFileId("FILE-UPGRADE-001"); + entity.setOriginalFilename("upgrade-package.zip"); + entity.setStoragePath(packagePath.toString()); + entity.setStatus("ACTIVE"); + return entity; + } + + private void tamperManifestChecksum(Path packagePath) throws Exception { + Path tempDir = Files.createTempDirectory("upgrade-manifest-tamper"); + UpgradePackageStagingService stagingService = stagingService(tempDir.resolve("staging")); + Path staged = stagingService.stage("FILE-TAMPER", packagePath); + UpgradeManifest manifest = objectMapper.readValue(Files.readString(staged.resolve("manifest.json")), UpgradeManifest.class); + manifest.getFileChecksums().setPayloadZip("deadbeef"); + byte[] manifestBytes = objectMapper.writeValueAsBytes(manifest); + byte[] payloadBytes = Files.readAllBytes(staged.resolve("payload.zip")); + byte[] signatureBytes = Files.readAllBytes(staged.resolve("signature.sig")); + + try (ZipOutputStream outputStream = new ZipOutputStream(Files.newOutputStream(packagePath))) { + outputStream.putNextEntry(new ZipEntry("manifest.json")); + outputStream.write(manifestBytes); + outputStream.closeEntry(); + + outputStream.putNextEntry(new ZipEntry("signature.sig")); + outputStream.write(signatureBytes); + outputStream.closeEntry(); + + outputStream.putNextEntry(new ZipEntry("payload.zip")); + outputStream.write(payloadBytes); + outputStream.closeEntry(); + } + } + + private static String sha256Hex(Path file) throws IOException, NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + digest.update(Files.readAllBytes(file)); + return HexFormat.of().formatHex(digest.digest()); + } + + private static DeviceSoftwareVersionEntity component(String code, String name, String version) { + DeviceSoftwareVersionEntity entity = new DeviceSoftwareVersionEntity(); + entity.setComponentCode(code); + entity.setComponentName(name); + entity.setCurrentVersion(version); + return entity; + } + + private static class PassThroughSignatureVerifier implements UpgradePackageSignatureVerifier { + @Override + public void verify(UpgradeManifest manifest, Path manifestPath, Path payloadZipPath, Path signaturePath) { + } + } + + private static class InMemoryFileRecordRepository implements FileRecordRepository { + private final Map store = new HashMap<>(); + + @Override + public Optional findByFileId(String fileId) { + return Optional.ofNullable(store.get(fileId)); + } + + @Override + public void save(FileRecordEntity entity) { + store.put(entity.getFileId(), entity); + } + } + + private static class InMemoryDeviceSoftwareVersionRepository implements DeviceSoftwareVersionRepository { + private final Map store = new HashMap<>(); + + @Override + public Optional findByComponentCode(String componentCode) { + return Optional.ofNullable(store.get(componentCode)); + } + + @Override + public void saveOrUpdate(DeviceSoftwareVersionEntity entity) { + store.put(entity.getComponentCode(), entity); + } + + public void save(DeviceSoftwareVersionEntity entity) { + saveOrUpdate(entity); + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradeServiceTest.java b/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradeServiceTest.java new file mode 100644 index 0000000..e138e27 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/upgrade/service/UpgradeServiceTest.java @@ -0,0 +1,209 @@ +package com.cisd.tms.modules.upgrade.service; + +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.modules.file.entity.FileRecordEntity; +import com.cisd.tms.modules.file.repository.FileRecordRepository; +import com.cisd.tms.modules.upgrade.config.UpgradeProperties; +import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest; +import com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradePreviewResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradeTaskPageResponse; +import com.cisd.tms.modules.upgrade.dto.UpgradeTaskResponse; +import com.cisd.tms.modules.upgrade.entity.UpgradeTaskEntity; +import com.cisd.tms.modules.upgrade.executor.UpgradeTaskRunner; +import com.cisd.tms.modules.upgrade.repository.UpgradeTaskRepository; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class UpgradeServiceTest { + + @Test + void shouldCreateUpgradeTaskFromPreviewResult() { + UpgradeTaskRepository repository = new InMemoryUpgradeTaskRepository(); + FileRecordRepository fileRecordRepository = Mockito.mock(FileRecordRepository.class); + UpgradePackageService packageService = Mockito.mock(UpgradePackageService.class); + UpgradeTaskRunner runner = Mockito.mock(UpgradeTaskRunner.class); + UpgradeProperties properties = properties(); + + FileRecordEntity fileRecord = new FileRecordEntity(); + fileRecord.setFileId("FILE-001"); + fileRecord.setOriginalFilename("upgrade-package.zip"); + Mockito.when(fileRecordRepository.findByFileId("FILE-001")).thenReturn(Optional.of(fileRecord)); + + UpgradePreviewResponse preview = new UpgradePreviewResponse(); + preview.setTargetComponent("STANDARD_APP"); + preview.setCurrentVersion("V1.3.1"); + preview.setTargetVersion("V1.3.2"); + preview.setAllowed(true); + preview.setVerifyPassed(true); + preview.setVerifyMessage("校验通过"); + Mockito.when(packageService.preview("FILE-001")).thenReturn(preview); + + UpgradeService service = new UpgradeService( + repository, + fileRecordRepository, + packageService, + runner, + properties, + Runnable::run, + new ConcurrentHashMap<>() + ); + + UpgradeCreateTaskRequest request = new UpgradeCreateTaskRequest(); + request.setFileId("FILE-001"); + + UpgradeCreateTaskResponse response = service.createTask(request); + + Assertions.assertEquals("PENDING_CONFIRM", response.getStatus()); + Assertions.assertEquals("STANDARD_APP", response.getTargetComponent()); + Assertions.assertEquals("V1.3.1", response.getCurrentVersion()); + Assertions.assertEquals("V1.3.2", response.getTargetVersion()); + Assertions.assertTrue(response.getTaskId().startsWith("UPG-")); + } + + @Test + void shouldRejectExecuteWhenAnotherTaskIsRunning() { + InMemoryUpgradeTaskRepository repository = new InMemoryUpgradeTaskRepository(); + repository.save(entity("UPG-001", "PENDING_CONFIRM")); + repository.save(entity("UPG-002", "RUNNING")); + + UpgradeService service = new UpgradeService( + repository, + Mockito.mock(FileRecordRepository.class), + Mockito.mock(UpgradePackageService.class), + Mockito.mock(UpgradeTaskRunner.class), + properties(), + Runnable::run, + new ConcurrentHashMap<>() + ); + + BizException exception = Assertions.assertThrows(BizException.class, () -> service.executeTask("UPG-001")); + + Assertions.assertEquals("another upgrade task is running", exception.getMessage()); + } + + @Test + void shouldPageUpgradeTasks() { + InMemoryUpgradeTaskRepository repository = new InMemoryUpgradeTaskRepository(); + repository.save(entity("UPG-001", "SUCCESS")); + repository.save(entity("UPG-002", "FAILED")); + + UpgradeService service = new UpgradeService( + repository, + Mockito.mock(FileRecordRepository.class), + Mockito.mock(UpgradePackageService.class), + Mockito.mock(UpgradeTaskRunner.class), + properties(), + Runnable::run, + new ConcurrentHashMap<>() + ); + + UpgradeTaskPageResponse page = service.listTasks(1, 10, "", ""); + + Assertions.assertEquals(2L, page.getTotal()); + Assertions.assertEquals(2, page.getItems().size()); + } + + @Test + void shouldReturnUpgradeTaskDetail() { + InMemoryUpgradeTaskRepository repository = new InMemoryUpgradeTaskRepository(); + repository.save(entity("UPG-001", "SUCCESS")); + + UpgradeService service = new UpgradeService( + repository, + Mockito.mock(FileRecordRepository.class), + Mockito.mock(UpgradePackageService.class), + Mockito.mock(UpgradeTaskRunner.class), + properties(), + Runnable::run, + new ConcurrentHashMap<>() + ); + + UpgradeTaskResponse response = service.getTask("UPG-001"); + + Assertions.assertEquals("UPG-001", response.getTaskId()); + Assertions.assertEquals("SUCCESS", response.getStatus()); + } + + private static UpgradeProperties properties() { + UpgradeProperties properties = new UpgradeProperties(); + properties.setLogDir("/home/tms/tmp/tms-upgrade-logs"); + return properties; + } + + private static UpgradeTaskEntity entity(String taskId, String status) { + UpgradeTaskEntity entity = new UpgradeTaskEntity(); + entity.setTaskId(taskId); + entity.setUpgradeMode("OFFLINE"); + entity.setTargetComponent("STANDARD_APP"); + entity.setPackageFileId("FILE-001"); + entity.setPackageName("upgrade-package.zip"); + entity.setCurrentVersion("V1.3.1"); + entity.setTargetVersion("V1.3.2"); + entity.setStatus(status); + entity.setVerifyPassed(Boolean.TRUE); + entity.setVerifyMessage("校验通过"); + entity.setCreateTime(LocalDateTime.now()); + entity.setUpdateTime(LocalDateTime.now()); + return entity; + } + + private static class InMemoryUpgradeTaskRepository implements UpgradeTaskRepository { + private final Map store = new ConcurrentHashMap<>(); + + @Override + public void save(UpgradeTaskEntity entity) { + store.put(entity.getTaskId(), entity); + } + + @Override + public Optional findByTaskId(String taskId) { + return Optional.ofNullable(store.get(taskId)); + } + + @Override + public Optional findRunningTask() { + return store.values().stream() + .filter(entity -> "RUNNING".equals(entity.getStatus())) + .findFirst(); + } + + @Override + public List findPage(String targetComponent, String status, int offset, int limit) { + return store.values().stream() + .filter(entity -> targetComponent == null || targetComponent.isBlank() || targetComponent.equals(entity.getTargetComponent())) + .filter(entity -> status == null || status.isBlank() || status.equals(entity.getStatus())) + .sorted(Comparator.comparing(UpgradeTaskEntity::getTaskId).reversed()) + .skip(offset) + .limit(limit) + .collect(Collectors.toCollection(ArrayList::new)); + } + + @Override + public long countPage(String targetComponent, String status) { + return findPage(targetComponent, status, 0, Integer.MAX_VALUE).size(); + } + + @Override + public void updateStatus(String taskId, String status, String errorMessage, String detailLogPath, LocalDateTime startedAt, LocalDateTime finishedAt) { + UpgradeTaskEntity entity = store.get(taskId); + if (entity == null) { + return; + } + entity.setStatus(status); + entity.setErrorMessage(errorMessage); + entity.setDetailLogPath(detailLogPath); + entity.setStartedAt(startedAt); + entity.setFinishedAt(finishedAt); + } + } +} diff --git a/src/test/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifierTest.java b/src/test/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifierTest.java new file mode 100644 index 0000000..c1d2e94 --- /dev/null +++ b/src/test/java/com/cisd/tms/modules/upgrade/support/PcieUpgradePackageSignatureVerifierTest.java @@ -0,0 +1,96 @@ +package com.cisd.tms.modules.upgrade.support; + +import com.cisd.tms.common.enums.ErrorCode; +import com.cisd.tms.common.exception.BizException; +import com.cisd.tms.integration.crypto.pcie.Gm0018AlgorithmIds; +import com.cisd.tms.integration.crypto.pcie.model.EccExternalVerifyRequest; +import com.cisd.tms.integration.crypto.pcie.service.PcieCryptoService; +import com.cisd.tms.modules.upgrade.config.UpgradeProperties; +import com.cisd.tms.modules.upgrade.dto.UpgradeManifest; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Base64; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +class PcieUpgradePackageSignatureVerifierTest { + + @Test + void shouldVerifyUpgradeManifestWithConfiguredIntegrityKey() throws Exception { + Path tempDir = Files.createTempDirectory("upgrade-signature-verifier"); + Path manifestPath = tempDir.resolve("manifest.json"); + Path payloadZipPath = tempDir.resolve("payload.zip"); + Path signaturePath = tempDir.resolve("signature.sig"); + Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8); + Files.write(payloadZipPath, new byte[] {0x01, 0x02}); + byte[] signatureBytes = new byte[128]; + Files.writeString(signaturePath, Base64.getEncoder().encodeToString(signatureBytes), StandardCharsets.UTF_8); + + UpgradeProperties properties = new UpgradeProperties(); + byte[] publicKeyBlob = new byte[132]; + properties.setSignaturePublicKeyBase64(Base64.getEncoder().encodeToString(publicKeyBlob)); + + PcieCryptoService cryptoService = Mockito.mock(PcieCryptoService.class); + PcieUpgradePackageSignatureVerifier verifier = new PcieUpgradePackageSignatureVerifier(properties, cryptoService); + + verifier.verify(new UpgradeManifest(), manifestPath, payloadZipPath, signaturePath); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(EccExternalVerifyRequest.class); + Mockito.verify(cryptoService).eccExternalVerify(requestCaptor.capture()); + EccExternalVerifyRequest request = requestCaptor.getValue(); + Assertions.assertEquals(Gm0018AlgorithmIds.SM2_SIGN, request.getAlgId()); + Assertions.assertArrayEquals(publicKeyBlob, request.getPublicKeyBlob()); + Assertions.assertArrayEquals(signatureBytes, request.getSignature()); + Assertions.assertArrayEquals(Files.readAllBytes(manifestPath), request.getData()); + } + + @Test + void shouldRejectWhenIntegrityKeyIsNotConfigured() throws Exception { + Path tempDir = Files.createTempDirectory("upgrade-signature-verifier"); + Path manifestPath = tempDir.resolve("manifest.json"); + Path payloadZipPath = tempDir.resolve("payload.zip"); + Path signaturePath = tempDir.resolve("signature.sig"); + Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8); + Files.write(payloadZipPath, new byte[] {0x01, 0x02}); + Files.write(signaturePath, new byte[128]); + + PcieUpgradePackageSignatureVerifier verifier = + new PcieUpgradePackageSignatureVerifier(new UpgradeProperties(), Mockito.mock(PcieCryptoService.class)); + + BizException exception = Assertions.assertThrows( + BizException.class, + () -> verifier.verify(new UpgradeManifest(), manifestPath, payloadZipPath, signaturePath) + ); + + Assertions.assertEquals(ErrorCode.BIZ_ERROR.getCode(), exception.getCode()); + Assertions.assertEquals("upgrade signature public key not configured", exception.getMessage()); + } + + @Test + void shouldWrapCryptoCardVerifyFailure() throws Exception { + Path tempDir = Files.createTempDirectory("upgrade-signature-verifier"); + Path manifestPath = tempDir.resolve("manifest.json"); + Path payloadZipPath = tempDir.resolve("payload.zip"); + Path signaturePath = tempDir.resolve("signature.sig"); + Files.writeString(manifestPath, "{\"packageId\":\"PKG-001\"}", StandardCharsets.UTF_8); + Files.write(payloadZipPath, new byte[] {0x01, 0x02}); + Files.write(signaturePath, new byte[128]); + + UpgradeProperties properties = new UpgradeProperties(); + properties.setSignaturePublicKeyBase64(Base64.getEncoder().encodeToString(new byte[132])); + PcieCryptoService cryptoService = Mockito.mock(PcieCryptoService.class); + Mockito.doThrow(new IllegalStateException("verify failed")).when(cryptoService).eccExternalVerify(Mockito.any()); + PcieUpgradePackageSignatureVerifier verifier = new PcieUpgradePackageSignatureVerifier(properties, cryptoService); + + BizException exception = Assertions.assertThrows( + BizException.class, + () -> verifier.verify(new UpgradeManifest(), manifestPath, payloadZipPath, signaturePath) + ); + + Assertions.assertEquals(ErrorCode.CRYPTO_CARD_ERROR.getCode(), exception.getCode()); + Assertions.assertEquals("upgrade package signature verification failed", exception.getMessage()); + } +} diff --git a/src/test/java/com/cisd/tms/scripts/TmsScriptTest.java b/src/test/java/com/cisd/tms/scripts/TmsScriptTest.java new file mode 100644 index 0000000..f4de0e3 --- /dev/null +++ b/src/test/java/com/cisd/tms/scripts/TmsScriptTest.java @@ -0,0 +1,135 @@ +package com.cisd.tms.scripts; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class TmsScriptTest { + + @Test + void shouldNotPassProfileOverrideWhenSpringProfilesActiveIsUnset() throws Exception { + Path appHome = Files.createTempDirectory("tms-script-no-profile"); + + ScriptRunResult result = runStart(appHome, null); + try { + Assertions.assertTrue( + result.javaArgs().stream().noneMatch(arg -> arg.startsWith("--spring.profiles.active=")) + ); + } finally { + runStop(appHome, null); + } + } + + @Test + void shouldPassProfileOverrideWhenSpringProfilesActiveIsProvided() throws Exception { + Path appHome = Files.createTempDirectory("tms-script-explicit-profile"); + + ScriptRunResult result = runStart(appHome, "dev"); + try { + Assertions.assertTrue(result.javaArgs().contains("--spring.profiles.active=dev")); + } finally { + runStop(appHome, "dev"); + } + } + + private static ScriptRunResult runStart(Path appHome, String profileOverride) throws Exception { + Path argsLog = prepareAppHome(appHome); + ProcessBuilder processBuilder = new ProcessBuilder( + "/bin/bash", + scriptPath().toString(), + "start" + ); + processBuilder.directory(repoRoot().toFile()); + processBuilder.environment().put("APP_HOME", appHome.toString()); + processBuilder.environment().put("JAVA_BIN", createFakeJava(appHome, argsLog).toString()); + if (profileOverride == null) { + processBuilder.environment().remove("SPRING_PROFILES_ACTIVE"); + } else { + processBuilder.environment().put("SPRING_PROFILES_ACTIVE", profileOverride); + } + + Process process = processBuilder.start(); + int exitCode = process.waitFor(); + + Assertions.assertEquals(0, exitCode, readProcessOutput(process)); + + Path pidFile = appHome.resolve("run/tms-framework.pid"); + waitForFile(pidFile); + waitForFile(argsLog); + return new ScriptRunResult(Files.readAllLines(argsLog)); + } + + private static void runStop(Path appHome, String profileOverride) throws Exception { + ProcessBuilder processBuilder = new ProcessBuilder( + "/bin/bash", + scriptPath().toString(), + "stop" + ); + processBuilder.directory(repoRoot().toFile()); + processBuilder.environment().put("APP_HOME", appHome.toString()); + if (profileOverride == null) { + processBuilder.environment().remove("SPRING_PROFILES_ACTIVE"); + } else { + processBuilder.environment().put("SPRING_PROFILES_ACTIVE", profileOverride); + } + + Process process = processBuilder.start(); + int exitCode = process.waitFor(); + Assertions.assertEquals(0, exitCode, readProcessOutput(process)); + } + + private static Path prepareAppHome(Path appHome) throws IOException { + Files.createDirectories(appHome.resolve("config")); + Files.writeString(appHome.resolve("config/application.yml"), "spring:\n application:\n name: test\n"); + Files.createDirectories(appHome.resolve("logs")); + Files.createDirectories(appHome.resolve("run")); + Files.writeString(appHome.resolve("tms-framework.jar"), "fake"); + return appHome.resolve("java-args.log"); + } + + private static Path createFakeJava(Path appHome, Path argsLog) throws IOException { + Path fakeJava = appHome.resolve("fake-java.sh"); + String script = """ + #!/usr/bin/env bash + printf '%s\n' "$@" > '__ARGS_LOG__' + trap 'exit 0' TERM INT + while true; do + read -r -t 1 _ || true + done + """.replace("__ARGS_LOG__", argsLog.toString()); + Files.writeString(fakeJava, script); + fakeJava.toFile().setExecutable(true); + return fakeJava; + } + + private static void waitForFile(Path path) throws Exception { + long deadline = System.nanoTime() + Duration.ofSeconds(5).toNanos(); + while (System.nanoTime() < deadline) { + if (Files.exists(path) && Files.size(path) >= 0) { + return; + } + Thread.sleep(50); + } + Assertions.fail("Timed out waiting for file: " + path); + } + + private static String readProcessOutput(Process process) throws IOException { + return new String(process.getInputStream().readAllBytes()) + + new String(process.getErrorStream().readAllBytes()); + } + + private static Path repoRoot() { + return Path.of("").toAbsolutePath().normalize(); + } + + private static Path scriptPath() { + return repoRoot().resolve("scripts/tms.sh"); + } + + private record ScriptRunResult(List javaArgs) { + } +} diff --git a/src/test/java/com/cisd/tms/security/internal/InternalApiAuthInterceptorTest.java b/src/test/java/com/cisd/tms/security/internal/InternalApiAuthInterceptorTest.java new file mode 100644 index 0000000..220da9a --- /dev/null +++ b/src/test/java/com/cisd/tms/security/internal/InternalApiAuthInterceptorTest.java @@ -0,0 +1,153 @@ +package com.cisd.tms.security.internal; + +import com.cisd.tms.common.config.properties.TmsSecurityProperties; +import com.cisd.tms.modules.auth.entity.AuthSessionEntity; +import com.cisd.tms.modules.auth.repository.AuthSessionRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.Clock; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +class InternalApiAuthInterceptorTest { + + private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T02:00:00Z"), ZoneOffset.UTC); + + @Test + void shouldRejectRequestWhenSessionTokenIsMissing() throws Exception { + InternalApiAuthInterceptor interceptor = new InternalApiAuthInterceptor( + new InMemoryAuthSessionRepository(), + new ObjectMapper(), + new TmsSecurityProperties(), + FIXED_CLOCK + ); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/device/info"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, new Object()); + + Assertions.assertFalse(allowed); + Assertions.assertEquals(401, response.getStatus()); + Assertions.assertTrue(response.getContentAsString().contains("session token missing")); + } + + @Test + void shouldRejectRequestWhenSessionHasExpiredByIdleTimeout() throws Exception { + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + sessions.save(session("token-expired", "KEY_ADMIN", "FULL", + LocalDateTime.of(2026, 3, 23, 1, 45), + LocalDateTime.of(2026, 3, 23, 1, 49), + LocalDateTime.of(2026, 3, 23, 1, 59))); + + InternalApiAuthInterceptor interceptor = new InternalApiAuthInterceptor( + sessions, + new ObjectMapper(), + new TmsSecurityProperties(), + FIXED_CLOCK + ); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/device/info"); + request.addHeader("Authorization", "Bearer token-expired"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, new Object()); + + Assertions.assertFalse(allowed); + Assertions.assertEquals(401, response.getStatus()); + Assertions.assertTrue(response.getContentAsString().contains("session expired")); + } + + @Test + void shouldAllowRequestAndRefreshLastActiveAtForValidSession() throws Exception { + InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository(); + sessions.save(session("token-valid", "AUDIT_ADMIN", "LIMITED", + LocalDateTime.of(2026, 3, 23, 1, 55), + LocalDateTime.of(2026, 3, 23, 1, 58), + LocalDateTime.of(2026, 3, 23, 2, 8))); + + InternalApiAuthInterceptor interceptor = new InternalApiAuthInterceptor( + sessions, + new ObjectMapper(), + new TmsSecurityProperties(), + FIXED_CLOCK + ); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/device/info"); + request.addHeader("Authorization", "Bearer token-valid"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, new Object()); + + Assertions.assertTrue(allowed); + Assertions.assertEquals("AUDIT_ADMIN", request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE)); + Assertions.assertEquals("LIMITED", request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL)); + AuthSessionEntity updated = sessions.findBySessionToken("token-valid").orElseThrow(); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), updated.getLastActiveAt()); + Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), updated.getExpiresAt()); + } + + @Test + void shouldBypassSessionValidationWhenInternalAuthIsDisabled() throws Exception { + TmsSecurityProperties securityProperties = new TmsSecurityProperties(); + securityProperties.getInternalAuth().setEnabled(false); + + InternalApiAuthInterceptor interceptor = new InternalApiAuthInterceptor( + new InMemoryAuthSessionRepository(), + new ObjectMapper(), + securityProperties, + FIXED_CLOCK + ); + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/device/info"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + boolean allowed = interceptor.preHandle(request, response, new Object()); + + Assertions.assertTrue(allowed); + Assertions.assertEquals("KEY_ADMIN", request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE)); + Assertions.assertEquals("FULL", request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL)); + Assertions.assertEquals("DEBUG-BYPASS", request.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN)); + } + + private static AuthSessionEntity session( + String token, + String roleCode, + String authLevel, + LocalDateTime issuedAt, + LocalDateTime lastActiveAt, + LocalDateTime expiresAt + ) { + AuthSessionEntity entity = new AuthSessionEntity(); + entity.setId((long) (token.hashCode() & Integer.MAX_VALUE)); + entity.setSessionToken(token); + entity.setRoleCode(roleCode); + entity.setAuthLevel(authLevel); + entity.setIssuedAt(issuedAt); + entity.setLastActiveAt(lastActiveAt); + entity.setExpiresAt(expiresAt); + return entity; + } + + private static class InMemoryAuthSessionRepository implements AuthSessionRepository { + private final Map store = new ConcurrentHashMap<>(); + + @Override + public Optional findBySessionToken(String sessionToken) { + return Optional.ofNullable(store.get(sessionToken)); + } + + @Override + public void save(AuthSessionEntity entity) { + store.put(entity.getSessionToken(), entity); + } + + @Override + public void update(AuthSessionEntity entity) { + store.put(entity.getSessionToken(), entity); + } + } +}