fix(web-ai): store encryption key in database
This commit is contained in:
-9
@@ -11,7 +11,6 @@ public class AihrWebAiProperties {
|
||||
private String provider = "tavily";
|
||||
private String endpoint = "https://api.tavily.com/search";
|
||||
private String apiKey = "";
|
||||
private String secretKey = "";
|
||||
private int maxResults = 5;
|
||||
|
||||
public boolean isEnabled() {
|
||||
@@ -46,14 +45,6 @@ public class AihrWebAiProperties {
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
public String getSecretKey() {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
public void setSecretKey(String secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
public int getMaxResults() {
|
||||
return maxResults;
|
||||
}
|
||||
|
||||
+34
-6
@@ -2,6 +2,7 @@ package org.dromara.aihr.webai;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
@@ -17,9 +18,11 @@ import java.util.Base64;
|
||||
class AihrWebSecretCodec {
|
||||
|
||||
private static final String PREFIX = "enc:v1:";
|
||||
private static final String CONFIG_KEY = "provider-encryption-v1";
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
private final AihrWebAiProperties properties;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private volatile SecretKeySpec databaseKey;
|
||||
|
||||
String encrypt(String plaintext) {
|
||||
if (plaintext == null || plaintext.isBlank()) {
|
||||
@@ -69,11 +72,36 @@ class AihrWebSecretCodec {
|
||||
}
|
||||
|
||||
private SecretKeySpec key() throws Exception {
|
||||
String secret = properties.getSecretKey() == null ? "" : properties.getSecretKey().trim();
|
||||
if (secret.length() < 16) {
|
||||
throw new ServiceException("请先配置至少16位的 AIHR_WEB_AI_SECRET_KEY");
|
||||
if (databaseKey != null) {
|
||||
return databaseKey;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (databaseKey != null) {
|
||||
return databaseKey;
|
||||
}
|
||||
jdbcTemplate.execute("""
|
||||
CREATE TABLE IF NOT EXISTS `aihr_web_ai_secret` (
|
||||
`config_key` varchar(64) NOT NULL,
|
||||
`config_value` varchar(255) NOT NULL,
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`config_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
|
||||
""");
|
||||
byte[] random = new byte[32];
|
||||
RANDOM.nextBytes(random);
|
||||
jdbcTemplate.update("""
|
||||
insert ignore into aihr_web_ai_secret (config_key, config_value, create_time)
|
||||
values (?, ?, now())
|
||||
""", CONFIG_KEY, Base64.getEncoder().encodeToString(random));
|
||||
String secret = jdbcTemplate.queryForObject("""
|
||||
select config_value from aihr_web_ai_secret where config_key = ?
|
||||
""", String.class, CONFIG_KEY);
|
||||
if (secret == null || secret.length() < 16) {
|
||||
throw new ServiceException("全网检索数据库主密钥无效");
|
||||
}
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256").digest(secret.getBytes(StandardCharsets.UTF_8));
|
||||
databaseKey = new SecretKeySpec(digest, "AES");
|
||||
return databaseKey;
|
||||
}
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256").digest(secret.getBytes(StandardCharsets.UTF_8));
|
||||
return new SecretKeySpec(digest, "AES");
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -22,6 +22,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -172,9 +174,10 @@ class AihrWebAiSecurityTest {
|
||||
@Test
|
||||
@Tag("dev")
|
||||
void databaseProviderSecretUsesAuthenticatedEncryption() {
|
||||
AihrWebAiProperties properties = new AihrWebAiProperties();
|
||||
properties.setSecretKey("unit-test-secret-key-32-bytes-long");
|
||||
AihrWebSecretCodec codec = new AihrWebSecretCodec(properties);
|
||||
JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
|
||||
when(jdbcTemplate.queryForObject(anyString(), eq(String.class), eq("provider-encryption-v1")))
|
||||
.thenReturn("unit-test-secret-key-32-bytes-long");
|
||||
AihrWebSecretCodec codec = new AihrWebSecretCodec(jdbcTemplate);
|
||||
|
||||
String encrypted = codec.encrypt("tvly-test-secret");
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 全网 AI 提供方密钥的数据库主密钥;不通过环境变量或通用参数接口暴露。
|
||||
CREATE TABLE IF NOT EXISTS `aihr_web_ai_secret` (
|
||||
`config_key` varchar(64) NOT NULL COMMENT '内部配置键',
|
||||
`config_value` varchar(255) NOT NULL COMMENT '随机主密钥,仅后端读取',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`config_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='全网AI内部密钥配置';
|
||||
|
||||
INSERT IGNORE INTO `aihr_web_ai_secret` (`config_key`, `config_value`)
|
||||
VALUES ('provider-encryption-v1', TO_BASE64(RANDOM_BYTES(32)));
|
||||
Reference in New Issue
Block a user