|
|
|
@ -1,12 +1,18 @@ |
|
|
|
package org.dromara.system.service.impl; |
|
|
|
|
|
|
|
import cn.hutool.core.date.DateUtil; |
|
|
|
import cn.hutool.core.util.HexUtil; |
|
|
|
import cn.hutool.core.util.StrUtil; |
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|
|
|
import lombok.RequiredArgsConstructor; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.dromara.common.mybatis.helper.DataPermissionHelper; |
|
|
|
import org.dromara.common.core.exception.ServiceException; |
|
|
|
import org.dromara.common.satoken.utils.LoginHelper; |
|
|
|
import org.dromara.system.domain.SysOperLog; |
|
|
|
import org.dromara.system.domain.SysUser; |
|
|
|
import org.dromara.system.domain.bean.CryptoIntegrityData; |
|
|
|
import org.dromara.system.domain.bean.CryptoOperationAudit; |
|
|
|
import org.dromara.system.domain.bean.CryptoSensitiveData; |
|
|
|
@ -14,8 +20,11 @@ import org.dromara.system.domain.vo.SymmetryEncryptVo; |
|
|
|
import org.dromara.system.mapper.CryptoIntegrityDataMapper; |
|
|
|
import org.dromara.system.mapper.CryptoOperationAuditMapper; |
|
|
|
import org.dromara.system.mapper.CryptoSensitiveDataMapper; |
|
|
|
import org.dromara.system.mapper.SysOperLogMapper; |
|
|
|
import org.dromara.system.mapper.SysUserMapper; |
|
|
|
import org.dromara.system.service.ICryptoSensitiveHsmService; |
|
|
|
import org.dromara.system.service.IPasswordPartyService; |
|
|
|
import org.springframework.scheduling.annotation.Async; |
|
|
|
import org.springframework.stereotype.Service; |
|
|
|
import org.springframework.transaction.annotation.Transactional; |
|
|
|
|
|
|
|
@ -31,9 +40,12 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService |
|
|
|
private final CryptoSensitiveDataMapper cryptoSensitiveDataMapper; |
|
|
|
private final CryptoOperationAuditMapper cryptoOperationAuditMapper; |
|
|
|
private final CryptoIntegrityDataMapper cryptoIntegrityDataMapper; |
|
|
|
private final SysUserMapper sysUserMapper; |
|
|
|
private final SysOperLogMapper sysOperLogMapper; |
|
|
|
|
|
|
|
private static final String KEY_VERSION = "V1"; |
|
|
|
private static final String TABLE_SYS_USER = "sys_user"; |
|
|
|
private static final String TABLE_SYS_OPER_LOG = "sys_oper_log"; |
|
|
|
|
|
|
|
@Override |
|
|
|
@Transactional(rollbackFor = Exception.class) |
|
|
|
@ -111,40 +123,152 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService |
|
|
|
@Override |
|
|
|
@Transactional(rollbackFor = Exception.class) |
|
|
|
public void saveIntegrityHash(String tableName, String bizId, String fieldList, String... plainValues) { |
|
|
|
StringBuilder sb = new StringBuilder(); |
|
|
|
for (int i = 0; i < plainValues.length; i++) { |
|
|
|
if (i > 0) sb.append(","); |
|
|
|
sb.append(plainValues[i] != null ? plainValues[i] : ""); |
|
|
|
try{ |
|
|
|
String hmacHash = calculateHash(fieldList, plainValues); |
|
|
|
|
|
|
|
LambdaQueryWrapper<CryptoIntegrityData> lqw = new LambdaQueryWrapper<>(); |
|
|
|
lqw.eq(CryptoIntegrityData::getTableName, tableName) |
|
|
|
.eq(CryptoIntegrityData::getBizId, bizId) |
|
|
|
.eq(CryptoIntegrityData::getIsDeleted, 0); |
|
|
|
CryptoIntegrityData existing = cryptoIntegrityDataMapper.selectOne(lqw); |
|
|
|
|
|
|
|
if (existing != null) { |
|
|
|
existing.setFieldList(fieldList); |
|
|
|
existing.setSm3Hash(hmacHash); |
|
|
|
existing.setHashAlg("SM3"); |
|
|
|
Date date = new Date(); |
|
|
|
existing.setGenerateTime(date); |
|
|
|
existing.setLastVerifyTime(date); |
|
|
|
existing.setVerifyResult(1); |
|
|
|
cryptoIntegrityDataMapper.updateById(existing); |
|
|
|
} else { |
|
|
|
CryptoIntegrityData record = new CryptoIntegrityData(); |
|
|
|
record.setTableName(tableName); |
|
|
|
record.setBizId(bizId); |
|
|
|
record.setFieldList(fieldList); |
|
|
|
record.setSm3Hash(hmacHash); |
|
|
|
record.setHashAlg("SM3"); |
|
|
|
Date date = new Date(); |
|
|
|
record.setGenerateTime(date); |
|
|
|
record.setLastVerifyTime(date); |
|
|
|
record.setVerifyResult(1); |
|
|
|
record.setIsDeleted(0); |
|
|
|
cryptoIntegrityDataMapper.insert(record); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("保存数据完整性摘要失败, tableName={}, bizId={}", tableName, bizId, e); |
|
|
|
} |
|
|
|
String concatPlain = sb.toString(); |
|
|
|
String inputHex = HexUtil.encodeHexStr(concatPlain.getBytes(StandardCharsets.UTF_8)); |
|
|
|
String hmacHash = passwordPartyService.hmacWithIndex(inputHex); |
|
|
|
} |
|
|
|
|
|
|
|
LambdaQueryWrapper<CryptoIntegrityData> lqw = new LambdaQueryWrapper<>(); |
|
|
|
lqw.eq(CryptoIntegrityData::getTableName, tableName) |
|
|
|
.eq(CryptoIntegrityData::getBizId, bizId) |
|
|
|
.eq(CryptoIntegrityData::getIsDeleted, 0); |
|
|
|
CryptoIntegrityData existing = cryptoIntegrityDataMapper.selectOne(lqw); |
|
|
|
@Override |
|
|
|
public int verifyIntegrityHash(String tableName) { |
|
|
|
if (!TABLE_SYS_USER.equals(tableName) && !TABLE_SYS_OPER_LOG.equals(tableName)) { |
|
|
|
throw new ServiceException("暂不支持该业务表的完整性校验: " + tableName); |
|
|
|
} |
|
|
|
Date today = DateUtil.beginOfDay(new Date()); |
|
|
|
|
|
|
|
if (existing != null) { |
|
|
|
existing.setFieldList(fieldList); |
|
|
|
existing.setSm3Hash(hmacHash); |
|
|
|
existing.setHashAlg("SM3"); |
|
|
|
existing.setGenerateTime(new Date()); |
|
|
|
existing.setVerifyResult(0); |
|
|
|
cryptoIntegrityDataMapper.updateById(existing); |
|
|
|
} else { |
|
|
|
CryptoIntegrityData record = new CryptoIntegrityData(); |
|
|
|
record.setTableName(tableName); |
|
|
|
record.setBizId(bizId); |
|
|
|
record.setFieldList(fieldList); |
|
|
|
record.setSm3Hash(hmacHash); |
|
|
|
record.setHashAlg("SM3"); |
|
|
|
record.setGenerateTime(new Date()); |
|
|
|
record.setVerifyResult(0); |
|
|
|
record.setIsDeleted(0); |
|
|
|
cryptoIntegrityDataMapper.insert(record); |
|
|
|
int processed = 0; |
|
|
|
int passed = 0; |
|
|
|
int tampered = 0; |
|
|
|
long pageSize = 500L; |
|
|
|
Page<CryptoIntegrityData> page = new Page<>(1, pageSize); |
|
|
|
LambdaQueryWrapper<CryptoIntegrityData> lqw = new LambdaQueryWrapper<CryptoIntegrityData>() |
|
|
|
.eq(CryptoIntegrityData::getTableName, tableName) |
|
|
|
.eq(CryptoIntegrityData::getIsDeleted, 0) |
|
|
|
.lt(CryptoIntegrityData::getGenerateTime, today); |
|
|
|
while (true) { |
|
|
|
Page<CryptoIntegrityData> result = cryptoIntegrityDataMapper.selectPage(page, lqw); |
|
|
|
for (CryptoIntegrityData record : result.getRecords()) { |
|
|
|
try { |
|
|
|
boolean matched = TABLE_SYS_USER.equals(tableName) |
|
|
|
? verifySysUserIntegrity(record) |
|
|
|
: verifySysOperLogIntegrity(record); |
|
|
|
record.setVerifyResult(matched ? 1 : 2); |
|
|
|
record.setLastVerifyTime(new Date()); |
|
|
|
cryptoIntegrityDataMapper.updateById(record); |
|
|
|
processed++; |
|
|
|
if (matched) { |
|
|
|
passed++; |
|
|
|
} else { |
|
|
|
tampered++; |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("完整性校验失败, tableName={}, bizId={}", record.getTableName(), record.getBizId(), e); |
|
|
|
} |
|
|
|
} |
|
|
|
if (!result.hasNext()) { |
|
|
|
break; |
|
|
|
} |
|
|
|
page.setCurrent(page.getCurrent() + 1); |
|
|
|
} |
|
|
|
log.info("完整性校验完成, tableName={}, 处理={}, 通过={}, 异常={}", tableName, processed, passed, tampered); |
|
|
|
return processed; |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public int syncIntegrityHashModifiedUsers() { |
|
|
|
Date begin = DateUtil.beginOfDay(DateUtil.yesterday()); |
|
|
|
Date end = DateUtil.endOfDay(DateUtil.yesterday()); |
|
|
|
|
|
|
|
int processed = 0; |
|
|
|
long pageSize = 500L; |
|
|
|
Page<SysUser> page = new Page<>(1, pageSize); |
|
|
|
while (true) { |
|
|
|
Page<SysUser> result = DataPermissionHelper.ignore( |
|
|
|
() -> sysUserMapper.selectModifiedUserByUpdateTime(page, begin, end)); |
|
|
|
for (SysUser user : result.getRecords()) { |
|
|
|
try { |
|
|
|
saveIntegrityHash(TABLE_SYS_USER, String.valueOf(user.getUserId()), |
|
|
|
"password,phonenumber,certificateNumber", |
|
|
|
user.getPassword() != null ? user.getPassword() : "", |
|
|
|
user.getPhonenumber() != null ? user.getPhonenumber() : "", |
|
|
|
user.getCertificateNumber() != null ? user.getCertificateNumber() : ""); |
|
|
|
processed++; |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("同步前一天修改用户完整性摘要失败, userId={}", user.getUserId(), e); |
|
|
|
} |
|
|
|
} |
|
|
|
if (!result.hasNext()) { |
|
|
|
break; |
|
|
|
} |
|
|
|
page.setCurrent(page.getCurrent() + 1); |
|
|
|
} |
|
|
|
log.info("同步前一天修改用户完整性摘要完成, 处理数量={}", processed); |
|
|
|
return processed; |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public int syncIntegrityHashModifiedOperLogs() { |
|
|
|
Date begin = DateUtil.beginOfDay(DateUtil.yesterday()); |
|
|
|
Date end = DateUtil.endOfDay(DateUtil.yesterday()); |
|
|
|
|
|
|
|
int processed = 0; |
|
|
|
long pageSize = 500L; |
|
|
|
Page<SysOperLog> page = new Page<>(1, pageSize); |
|
|
|
while (true) { |
|
|
|
Page<SysOperLog> result = DataPermissionHelper.ignore( |
|
|
|
() -> sysOperLogMapper.selectByOperTimeRange(page, begin, end)); |
|
|
|
for (SysOperLog operLog : result.getRecords()) { |
|
|
|
try { |
|
|
|
saveIntegrityHash(TABLE_SYS_OPER_LOG, String.valueOf(operLog.getOperId()), |
|
|
|
"business_type,method,oper_name,oper_param,oper_time", |
|
|
|
toStr(operLog.getBusinessType()), |
|
|
|
toStr(operLog.getMethod()), |
|
|
|
toStr(operLog.getOperName()), |
|
|
|
toStr(operLog.getOperParam()), |
|
|
|
toStr(operLog.getOperTime())); |
|
|
|
processed++; |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("同步前一天操作日志完整性摘要失败, operId={}", operLog.getOperId(), e); |
|
|
|
} |
|
|
|
} |
|
|
|
if (!result.hasNext()) { |
|
|
|
break; |
|
|
|
} |
|
|
|
page.setCurrent(page.getCurrent() + 1); |
|
|
|
} |
|
|
|
log.info("同步前一天操作日志完整性摘要完成, 处理数量={}", processed); |
|
|
|
return processed; |
|
|
|
} |
|
|
|
|
|
|
|
private CryptoSensitiveData queryActiveRecord(String tableName, String bizId, String fieldCode) { |
|
|
|
@ -183,6 +307,16 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService |
|
|
|
cryptoOperationAuditMapper.insert(audit); |
|
|
|
} |
|
|
|
|
|
|
|
private String calculateHash(String fieldList, String... plainValues) { |
|
|
|
StringBuilder sb = new StringBuilder(); |
|
|
|
for (int i = 0; i < plainValues.length; i++) { |
|
|
|
if (i > 0) sb.append(","); |
|
|
|
sb.append(plainValues[i] != null ? plainValues[i] : ""); |
|
|
|
} |
|
|
|
String inputHex = HexUtil.encodeHexStr(sb.toString().getBytes(StandardCharsets.UTF_8)); |
|
|
|
return passwordPartyService.hmacWithIndex(inputHex); |
|
|
|
} |
|
|
|
|
|
|
|
private static byte[] hexToBytes(String hex) { |
|
|
|
int len = hex.length(); |
|
|
|
byte[] data = new byte[len / 2]; |
|
|
|
@ -192,4 +326,77 @@ public class CryptoSensitiveHsmServiceImpl implements ICryptoSensitiveHsmService |
|
|
|
} |
|
|
|
return data; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 校验 sys_user 表数据完整性 |
|
|
|
* <p> |
|
|
|
* 根据基准摘要记录的 biz_id 回查 sys_user 当前数据, |
|
|
|
* 按 field_list 解析参与字段(password、phonenumber、certificateNumber), |
|
|
|
* 重新计算 HMAC-SM3 摘要并与基准 sm3_hash 比对。 |
|
|
|
* |
|
|
|
* @param record 基准摘要记录 |
|
|
|
* @return true=校验通过,false=数据缺失或摘要不一致(疑似篡改) |
|
|
|
*/ |
|
|
|
private boolean verifySysUserIntegrity(CryptoIntegrityData record) { |
|
|
|
SysUser user = DataPermissionHelper.ignore( |
|
|
|
() -> sysUserMapper.selectById(Long.valueOf(record.getBizId()))); |
|
|
|
if (user == null) { |
|
|
|
log.warn("完整性校验: sys_user 数据不存在, bizId={}", record.getBizId()); |
|
|
|
return false; |
|
|
|
} |
|
|
|
String[] fields = record.getFieldList().split(","); |
|
|
|
String[] values = new String[fields.length]; |
|
|
|
for (int i = 0; i < fields.length; i++) { |
|
|
|
String field = fields[i].trim(); |
|
|
|
switch (field) { |
|
|
|
case "password" -> values[i] = user.getPassword() != null ? user.getPassword() : ""; |
|
|
|
case "phonenumber" -> values[i] = user.getPhonenumber() != null ? user.getPhonenumber() : ""; |
|
|
|
case "certificateNumber" -> values[i] = user.getCertificateNumber() != null ? user.getCertificateNumber() : ""; |
|
|
|
default -> throw new ServiceException("不支持的完整性校验字段: " + field); |
|
|
|
} |
|
|
|
} |
|
|
|
String currentHash = calculateHash(record.getFieldList(), values); |
|
|
|
boolean matched = currentHash.equals(record.getSm3Hash()); |
|
|
|
if (!matched) { |
|
|
|
log.warn("数据完整性校验未通过, 疑似篡改, tableName={}, bizId={}", record.getTableName(), record.getBizId()); |
|
|
|
} |
|
|
|
return matched; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 校验 sys_oper_log 表数据完整性 |
|
|
|
* <p> |
|
|
|
* 根据基准摘要记录的 biz_id 回查 sys_oper_log 当前数据, |
|
|
|
* 固定按 business_type、method、oper_name、oper_param、oper_time 五个字段 |
|
|
|
* 重新计算 HMAC-SM3 摘要并与基准 sm3_hash 比对。 |
|
|
|
* 非字符串字段统一按 String.valueOf 转换,与保存摘要时的取值口径保持一致。 |
|
|
|
* |
|
|
|
* @param record 基准摘要记录 |
|
|
|
* @return true=校验通过,false=数据缺失或摘要不一致(疑似篡改) |
|
|
|
*/ |
|
|
|
private boolean verifySysOperLogIntegrity(CryptoIntegrityData record) { |
|
|
|
SysOperLog operLog = DataPermissionHelper.ignore( |
|
|
|
() -> sysOperLogMapper.selectById(Long.valueOf(record.getBizId()))); |
|
|
|
if (operLog == null) { |
|
|
|
log.warn("完整性校验: sys_oper_log 数据不存在, bizId={}", record.getBizId()); |
|
|
|
return false; |
|
|
|
} |
|
|
|
String[] values = new String[]{ |
|
|
|
toStr(operLog.getBusinessType()), |
|
|
|
toStr(operLog.getMethod()), |
|
|
|
toStr(operLog.getOperName()), |
|
|
|
toStr(operLog.getOperParam()), |
|
|
|
toStr(operLog.getOperTime()) |
|
|
|
}; |
|
|
|
String currentHash = calculateHash(record.getFieldList(), values); |
|
|
|
boolean matched = currentHash.equals(record.getSm3Hash()); |
|
|
|
if (!matched) { |
|
|
|
log.warn("数据完整性校验未通过, 疑似篡改, tableName={}, bizId={}", record.getTableName(), record.getBizId()); |
|
|
|
} |
|
|
|
return matched; |
|
|
|
} |
|
|
|
|
|
|
|
private static String toStr(Object value) { |
|
|
|
return value != null ? String.valueOf(value) : ""; |
|
|
|
} |
|
|
|
} |
|
|
|
|