Overview
Applications frequently handle sensitive information such as contact numbers, government-isued IDs, and financial records. Storing these values in plaintext poses security risks, while manual encryption and decryption scatter boilerplate code across service layers. Furthermore, critical business data requires tamper-evidence mechanisms to guarantee consistency between storage and retrieval. This approach leverages Java custom annotations combined with a MyBatis Executor interceptor to centralize cryptographic transformations and integrity verification, keeping domain models clean and business logic unaffected.
Execution Workflow
- Read Operations (SELECT): After the SQL query executes, the interceptor intercepts the result set. Fields marked for integrity verification are validated against their stored signatures. If validation succeeds, fields marked for confidentiality are decrypted before returning to the caller.
- Write Operations (INSERT/UPDATE): Before executing the SQL statement, the interceptor examines the parameter object. Confidential fields are encrypted, and an HMAC signature is computed across integrity-bound fields and stored in a designated signature column.
Annotation Definitions
Custom annotations act as declarative markers for the security engine. The entity-level annotation identifies classes requiring protection, while the field-level annotation specifies the protection strategy.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProtectedEntity {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ProtectedField {
ProtectionMode[] modes() default ProtectionMode.ENCRYPT;
}
public enum ProtectionMode {
ENCRYPT,
INTEGRITY,
SIGNATURE
}
Interceptor Implementation
The interceptor attaches to the MyBatis Executor layer, capturing both query and update methods. It inspects the command type and delegates processing to dedicated reflection-based routines. The implementation abstracts cryptographic operations into clear method signatures, allowing seamless substitution with different algorithms (e.g., AES, SM4) or hardware security modules.
@Component
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class DataSecurityInterceptor implements Interceptor {
private static final Logger log = LoggerFactory.getLogger(DataSecurityInterceptor.class);
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement stmt = (MappedStatement) invocation.getArgs()[0];
SqlCommandType cmdType = stmt.getSqlCommandType();
Object payload = invocation.getArgs()[1];
if (payload == null) {
return invocation.proceed();
}
switch (cmdType) {
case SELECT:
Object result = invocation.proceed();
if (result == null) return null;
processReadOperation(result);
return result;
case INSERT:
case UPDATE:
payload = extractTargetObject(payload);
if (payload != null && isProtectedEntity(payload)) {
applyWriteProtection(payload);
}
return invocation.proceed();
default:
return invocation.proceed();
}
}
private Object extractTargetObject(Object rawParam) {
if (rawParam instanceof MapperMethod.ParamMap) {
MapperMethod.ParamMap> map = (MapperMethod.ParamMap>) rawParam;
return map.get("et") != null ? map.get("et") : map.get("param1");
}
return rawParam;
}
private boolean isProtectedEntity(Object target) {
return target.getClass().isAnnotationPresent(ProtectedEntity.class);
}
private void processReadOperation(Object result) {
if (result instanceof Collection) {
((Collection>) result).forEach(this::processSingleEntityRead);
} else if (isProtectedEntity(result)) {
processSingleEntityRead(result);
}
}
private void processSingleEntityRead(Object entity) {
if (!isProtectedEntity(entity)) return;
Map<string object=""> integrityValues = new LinkedHashMap<>();
String storedSignature = null;
try {
for (Field field : entity.getClass().getDeclaredFields()) {
if (!field.isAnnotationPresent(ProtectedField.class)) continue;
field.setAccessible(true);
Object val = field.get(entity);
ProtectionMode[] modes = field.getAnnotation(ProtectedField.class).modes();
Set<protectionmode> modeSet = Set.of(modes);
if (modeSet.contains(ProtectionMode.INTEGRITY) && val != null) {
integrityValues.put(field.getName(), val.toString());
}
if (modeSet.contains(ProtectionMode.SIGNATURE) && val != null) {
storedSignature = val.toString();
}
}
if (!integrityValues.isEmpty() && storedSignature != null) {
String computedSignature = computeHmac(integrityValues.values());
if (!constantTimeEquals(storedSignature, computedSignature)) {
handleTamperDetection(entity);
return; // Skip decryption if integrity is broken
}
}
// Decrypt confidential fields
for (Field field : entity.getClass().getDeclaredFields()) {
if (!field.isAnnotationPresent(ProtectedField.class)) continue;
field.setAccessible(true);
Object val = field.get(entity);
if (Set.of(field.getAnnotation(ProtectedField.class).modes()).contains(ProtectionMode.ENCRYPT) && val != null) {
try {
field.set(entity, decryptValue(val.toString()));
} catch (Exception e) {
log.warn("Decryption failed for field {}: {}", field.getName(), e.getMessage());
field.set(entity, "[DECRYPTION_ERROR]");
}
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Reflection access denied during read processing", e);
}
}
private void applyWriteProtection(Object entity) {
Map<string object=""> integrityValues = new LinkedHashMap<>();
try {
// Phase 1: Encrypt and collect integrity values
for (Field field : entity.getClass().getDeclaredFields()) {
if (!field.isAnnotationPresent(ProtectedField.class)) continue;
field.setAccessible(true);
Object val = field.get(entity);
ProtectionMode[] modes = field.getAnnotation(ProtectedField.class).modes();
Set<protectionmode> modeSet = Set.of(modes);
if (modeSet.contains(ProtectionMode.ENCRYPT) && val != null) {
field.set(entity, encryptValue(val.toString()));
}
if (modeSet.contains(ProtectionMode.INTEGRITY) && val != null) {
integrityValues.put(field.getName(), field.get(entity).toString());
}
}
// Phase 2: Compute and attach signature
if (!integrityValues.isEmpty()) {
String signature = computeHmac(integrityValues.values());
for (Field field : entity.getClass().getDeclaredFields()) {
if (!field.isAnnotationPresent(ProtectedField.class)) continue;
if (Set.of(field.getAnnotation(ProtectedField.class).modes()).contains(ProtectionMode.SIGNATURE)) {
field.setAccessible(true);
field.set(entity, signature);
break;
}
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Reflection access denied during write processing", e);
}
}
private void handleTamperDetection(Object entity) {
try {
for (Field field : entity.getClass().getDeclaredFields()) {
if (!field.isAnnotationPresent(ProtectedField.class)) continue;
if (Set.of(field.getAnnotation(ProtectedField.class).modes()).contains(ProtectionMode.INTEGRITY)) {
field.setAccessible(true);
field.set(entity, "[DATA_TAMPERED]");
}
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to mark tampered data", e);
}
}
// Placeholder cryptographic operations - replace with actual implementation
private String encryptValue(String plaintext) {
// e.g., SM4/AES encryption returning Base64 string
return plaintext;
}
private String decryptValue(String ciphertext) {
// e.g., SM4/AES decryption returning plaintext
return ciphertext;
}
private String computeHmac(Collection> values) {
// Concatenate values with delimiter and compute HMAC-SHA256/Base64
return "mock_hmac_signature";
}
private boolean constantTimeEquals(String a, String b) {
return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
}
}</protectionmode></string></protectionmode></string>
Entity Configuration Example
Applying the annotations requires zero changes to DAO or Service layers. The framework automatically routes traffic through the protection pipeline based on the SQL command type.
@ProtectedEntity
public class UserProfile implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
private String userId;
@SecurityField(modes = ProtectionMode.ENCRYPT)
private String email;
@SecurityField(modes = {ProtectionMode.ENCRYPT, ProtectionMode.INTEGRITY})
private String phoneNumber;
@SecurityField(modes = {ProtectionMode.ENCRYPT, ProtectionMode.INTEGRITY})
private String nationalId;
@SecurityField(modes = ProtectionMode.SIGNATURE)
private String integrityToken;
// Getters and Setters omitted for brevity
}
When persisting a UserProfile instance, the interceptor encrypts email, phoneNumber, and nationalId, then calculates an HMAC over the integrity-bound fields and writes it to integrityToken. During retrieval, the token is verified first; upon success, the encrypted column are transparently decrypted before the object reaches the application layer.
Implementation Note: This mechanism operates on raw JDBC result sets and parameter objects. It does not interact with MyBatis second-level cache or application-level caching layers. Cached data should be explicitly invalidated or bypassed to ensure cryptographic transformations remain consistent.