Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
*/
package com.alibaba.cloud.ai.dataagent;

import com.alibaba.cloud.ai.dataagent.config.EncryptionProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@EnableConfigurationProperties(EncryptionProperties.class)
@SpringBootApplication
public class DataAgentApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2024-2026 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.cloud.ai.dataagent.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* Encryption configuration properties for sensitive data storage.
*
* <p>
* The encryption key must be a 32-byte AES key encoded as Base64. Set via environment
* variable {@code DATA_AGENT_ENCRYPT_KEY} or configuration property
* {@code spring.ai.alibaba.data-agent.encrypt-key}.
* </p>
*
* <p>
* <strong>Backward Compatibility:</strong> When {@code encrypt-key} is not configured
* (empty/null), encryption is completely disabled and the system behaves identically to
* the pre-encryption version. This allows:
* </p>
* <ul>
* <li>Zero-impact upgrade: existing installations work without any configuration
* change</li>
* <li>Gradual migration: enable encryption when ready, historical data remains
* readable</li>
* <li>Rollback safety: removing the key disables encryption without data loss</li>
* </ul>
*
* <p>
* To generate a new key: {@code openssl rand -base64 32}
* </p>
*/
@Data
@ConfigurationProperties(prefix = "spring.ai.alibaba.data-agent")
public class EncryptionProperties {

/**
* Base64-encoded 32-byte AES key for encrypting sensitive data. If not set,
* encryption is disabled (NOT recommended for production).
*/
private String encryptKey;

/**
* Check if encryption is enabled (key is configured).
* @return true if encryptKey is set and non-empty
*/
public boolean isEncryptionEnabled() {
return encryptKey != null && !encryptKey.isEmpty();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
*/
package com.alibaba.cloud.ai.dataagent.service.aimodelconfig;

import com.alibaba.cloud.ai.dataagent.config.EncryptionProperties;
import com.alibaba.cloud.ai.dataagent.enums.ModelType;
import com.alibaba.cloud.ai.dataagent.converter.ModelConfigConverter;
import com.alibaba.cloud.ai.dataagent.dto.ModelConfigDTO;
import com.alibaba.cloud.ai.dataagent.entity.ModelConfig;
import com.alibaba.cloud.ai.dataagent.mapper.ModelConfigMapper;
import com.alibaba.cloud.ai.dataagent.util.CryptoUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -39,9 +41,15 @@ public class ModelConfigDataServiceImpl implements ModelConfigDataService {

private final ModelConfigMapper modelConfigMapper;

private final EncryptionProperties encryptionProperties;

@Override
public ModelConfig findById(Integer id) {
return modelConfigMapper.findById(id);
ModelConfig config = modelConfigMapper.findById(id);
if (config != null) {
decryptModelConfig(config);
}
return config;
}

@Transactional(rollbackFor = Exception.class)
Expand All @@ -61,14 +69,21 @@ public void switchActiveStatus(Integer id, ModelType type) {

@Override
public List<ModelConfigDTO> listConfigs() {
return modelConfigMapper.findAll().stream().map(ModelConfigConverter::toDTO).collect(Collectors.toList());
return modelConfigMapper.findAll()
.stream()
.peek(this::decryptModelConfig)
.map(ModelConfigConverter::toDTO)
.collect(Collectors.toList());
}

@Override
public void addConfig(ModelConfigDTO dto) {
clean(dto);
// Encrypt sensitive fields before saving
ModelConfig entity = toEntity(dto);
encryptModelConfig(entity);
// 只存库,不切换
modelConfigMapper.insert(toEntity(dto));
modelConfigMapper.insert(entity);
}

private void clean(ModelConfigDTO dto) {
Expand Down Expand Up @@ -104,7 +119,10 @@ public ModelConfig updateConfigInDb(ModelConfigDTO dto) {
mergeDtoToEntity(dto, entity);
entity.setUpdatedTime(LocalDateTime.now());

// 3. 更新数据库
// 3. Encrypt sensitive fields before updating
encryptModelConfig(entity);

// 4. 更新数据库
modelConfigMapper.updateById(entity);

return entity;
Expand Down Expand Up @@ -160,7 +178,67 @@ public ModelConfigDTO getActiveConfigByType(ModelType modelType) {
log.warn("Activation model configuration of type [{}] not found, attempting to downgrade...", modelType);
return null;
}
decryptModelConfig(entity);
return toDTO(entity);
}

/**
* Encrypt sensitive fields of a model config before persisting.
*
* <p>
* This method is called during add/update operations. When encryption is enabled,
* apiKey and proxyPassword are encrypted before being saved to database.
* </p>
* @param config the model config to encrypt
* @see CryptoUtil#encrypt(String, String)
*/
private void encryptModelConfig(ModelConfig config) {
if (!encryptionProperties.isEncryptionEnabled()) {
// Encryption not configured, skip (backward compatible)
return;
}
String key = encryptionProperties.getEncryptKey();
if (config.getApiKey() != null && !config.getApiKey().isEmpty()) {
config.setApiKey(CryptoUtil.encrypt(config.getApiKey(), key));
}
if (config.getProxyPassword() != null && !config.getProxyPassword().isEmpty()) {
config.setProxyPassword(CryptoUtil.encrypt(config.getProxyPassword(), key));
}
}

/**
* Decrypt sensitive fields of a model config after loading from database.
*
* <p>
* <strong>Backward Compatibility:</strong> Uses
* {@link CryptoUtil#decryptOrReturn(String, String)} to handle both encrypted and
* plaintext data gracefully:
* </p>
* <ul>
* <li>If data is encrypted (Base64 with valid GCM structure) → decrypt it</li>
* <li>If data is plaintext (legacy unencrypted) → return as-is</li>
* <li>If encryption is not configured → skip decryption entirely</li>
* </ul>
*
* <p>
* This ensures a smooth upgrade path: after enabling encryption, existing plaintext
* data remains readable, and new data will be encrypted on write.
* </p>
* @param config the model config to decrypt
* @see CryptoUtil#decryptOrReturn(String, String)
*/
private void decryptModelConfig(ModelConfig config) {
if (!encryptionProperties.isEncryptionEnabled()) {
// Encryption not configured, data is plaintext, skip
return;
}
String key = encryptionProperties.getEncryptKey();
if (config.getApiKey() != null && !config.getApiKey().isEmpty()) {
config.setApiKey(CryptoUtil.decryptOrReturn(config.getApiKey(), key));
}
if (config.getProxyPassword() != null && !config.getProxyPassword().isEmpty()) {
config.setProxyPassword(CryptoUtil.decryptOrReturn(config.getProxyPassword(), key));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.alibaba.cloud.ai.dataagent.connector.pool.DBConnectionPool;
import com.alibaba.cloud.ai.dataagent.connector.pool.DBConnectionPoolFactory;
import com.alibaba.cloud.ai.dataagent.entity.AgentDatasource;
import com.alibaba.cloud.ai.dataagent.config.EncryptionProperties;
import com.alibaba.cloud.ai.dataagent.entity.Datasource;
import com.alibaba.cloud.ai.dataagent.entity.LogicalRelation;
import com.alibaba.cloud.ai.dataagent.enums.ErrorCodeEnum;
Expand All @@ -33,6 +34,7 @@
import com.alibaba.cloud.ai.dataagent.service.datasource.DatasourceService;
import com.alibaba.cloud.ai.dataagent.service.datasource.handler.DatasourceTypeHandler;
import com.alibaba.cloud.ai.dataagent.service.datasource.handler.registry.DatasourceTypeHandlerRegistry;
import com.alibaba.cloud.ai.dataagent.util.CryptoUtil;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -64,24 +66,36 @@ public class DatasourceServiceImpl implements DatasourceService {

private final DatasourceTypeHandlerRegistry datasourceTypeHandlerRegistry;

private final EncryptionProperties encryptionProperties;

@Override
public List<Datasource> getAllDatasource() {
return datasourceMapper.selectAll();
List<Datasource> list = datasourceMapper.selectAll();
list.forEach(this::decryptDatasourcePassword);
return list;
}

@Override
public List<Datasource> getDatasourceByStatus(String status) {
return datasourceMapper.selectByStatus(status);
List<Datasource> list = datasourceMapper.selectByStatus(status);
list.forEach(this::decryptDatasourcePassword);
return list;
}

@Override
public List<Datasource> getDatasourceByType(String type) {
return datasourceMapper.selectByType(type);
List<Datasource> list = datasourceMapper.selectByType(type);
list.forEach(this::decryptDatasourcePassword);
return list;
}

@Override
public Datasource getDatasourceById(Integer id) {
return datasourceMapper.selectById(id);
Datasource datasource = datasourceMapper.selectById(id);
if (datasource != null) {
decryptDatasourcePassword(datasource);
}
return datasource;
}

@Override
Expand Down Expand Up @@ -109,6 +123,9 @@ public Datasource createDatasource(Datasource datasource) {
datasource.setUsername("");
}

// Encrypt sensitive fields before saving
encryptDatasource(datasource);

datasourceMapper.insert(datasource);
return datasource;
}
Expand Down Expand Up @@ -139,6 +156,9 @@ public Datasource updateDatasource(Integer id, Datasource datasource) {
datasource.setUsername("");
}

// Encrypt sensitive fields before updating
encryptDatasource(datasource);

datasourceMapper.updateById(datasource);
return datasource;
}
Expand Down Expand Up @@ -463,4 +483,63 @@ public List<LogicalRelation> saveLogicalRelations(Integer datasourceId, List<Log
return logicalRelationMapper.selectByDatasourceId(datasourceId);
}

/**
* Encrypt sensitive fields of a datasource before persisting.
*
* <p>
* This method is called during create/update operations. When encryption is enabled,
* password and connectionUrl are encrypted before being saved to database.
* </p>
* @param datasource the datasource to encrypt
* @see CryptoUtil#encrypt(String, String)
*/
private void encryptDatasource(Datasource datasource) {
if (!encryptionProperties.isEncryptionEnabled()) {
// Encryption not configured, skip (backward compatible)
return;
}
String key = encryptionProperties.getEncryptKey();
if (datasource.getPassword() != null && !datasource.getPassword().isEmpty()) {
datasource.setPassword(CryptoUtil.encrypt(datasource.getPassword(), key));
}
if (datasource.getConnectionUrl() != null && !datasource.getConnectionUrl().isEmpty()) {
datasource.setConnectionUrl(CryptoUtil.encrypt(datasource.getConnectionUrl(), key));
}
}

/**
* Decrypt sensitive fields of a datasource after loading from database.
*
* <p>
* <strong>Backward Compatibility:</strong> Uses
* {@link CryptoUtil#decryptOrReturn(String, String)} to handle both encrypted and
* plaintext data gracefully:
* </p>
* <ul>
* <li>If data is encrypted (Base64 with valid GCM structure) → decrypt it</li>
* <li>If data is plaintext (legacy unencrypted) → return as-is</li>
* <li>If encryption is not configured → skip decryption entirely</li>
* </ul>
*
* <p>
* This ensures a smooth upgrade path: after enabling encryption, existing plaintext
* data remains readable, and new data will be encrypted on write.
* </p>
* @param datasource the datasource to decrypt
* @see CryptoUtil#decryptOrReturn(String, String)
*/
private void decryptDatasourcePassword(Datasource datasource) {
if (!encryptionProperties.isEncryptionEnabled()) {
// Encryption not configured, data is plaintext, skip
return;
}
String key = encryptionProperties.getEncryptKey();
if (datasource.getPassword() != null && !datasource.getPassword().isEmpty()) {
datasource.setPassword(CryptoUtil.decryptOrReturn(datasource.getPassword(), key));
}
if (datasource.getConnectionUrl() != null && !datasource.getConnectionUrl().isEmpty()) {
datasource.setConnectionUrl(CryptoUtil.decryptOrReturn(datasource.getConnectionUrl(), key));
}
}

}
Loading
Loading