From 790f9092fe1f8c6fc39561a58699487550452695 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Tue, 23 Sep 2025 17:54:35 +0900 Subject: [PATCH] =?UTF-8?q?PTL=5FUSER=20=EC=95=94=ED=98=B8=ED=99=94=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9,=20SafeDB=20Wrapper=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + build.gradle | 19 ++-- .../portal/portaluser/entity/PortalUser.java | 15 ++- .../eactive/ext/kjb/safedb/SafeDBBroker.java | 19 ++++ .../eactive/ext/kjb/safedb/SafeDBColumns.java | 51 ++++++++++ .../eactive/ext/kjb/safedb/SafeDBFaker.java | 73 ++++++++++++++ .../kjb/safedb/SafeDBMNotRnnoConverter.java | 81 ++++++++++++++++ .../eactive/ext/kjb/safedb/SafeDBWrapper.java | 91 ++++++++++++++++++ .../kjb/safedb/SafeDBWrapperException.java | 46 +++++++++ .../kjb/safedb/SafeDBWrapperSdkException.java | 18 ++++ .../kjb/safedb/test/SafeDbWrapperTest.java | 95 +++++++++++++++++++ 11 files changed, 498 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/eactive/ext/kjb/safedb/SafeDBBroker.java create mode 100644 src/main/java/com/eactive/ext/kjb/safedb/SafeDBColumns.java create mode 100644 src/main/java/com/eactive/ext/kjb/safedb/SafeDBFaker.java create mode 100644 src/main/java/com/eactive/ext/kjb/safedb/SafeDBMNotRnnoConverter.java create mode 100644 src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapper.java create mode 100644 src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapperException.java create mode 100644 src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapperSdkException.java create mode 100644 src/test/java/com/eactive/ext/kjb/safedb/test/SafeDbWrapperTest.java diff --git a/.gitignore b/.gitignore index 6b285d2..2f40306 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ local.properties .idea # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +*.iml # User-specific stuff .idea/**/workspace.xml diff --git a/build.gradle b/build.gradle index de917f6..207dbfb 100644 --- a/build.gradle +++ b/build.gradle @@ -22,12 +22,6 @@ def hibernateVersion = "5.6.15.Final" def generatedJavaDir = "$buildDir/generated/java" def generatedTestJavaDir = "$buildDir/generated-test/java" -repositories { - maven { - url "${nexusUrl}/repository/maven-public/" - allowInsecureProtocol = true - } -} java { toolchain { @@ -112,6 +106,11 @@ dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.8.2' + + + // 광주은행 safedb 용 임시 라이브러리 + compileOnly(fileTree(dir: "tmp/safedb", include: ["*.jar"])) +// implementation(fileTree(dir: "tmp/safedb", include: ["*.jar"])) } test { @@ -187,3 +186,11 @@ eclipse { tasks.named('eclipse').configure { finalizedBy 'compileJava', 'compileTestJava' } + +idea { + module { + generatedSourceDirs.add(file(generatedJavaDir)) + generatedSourceDirs.add(file(generatedTestJavaDir)) + } + +} \ No newline at end of file diff --git a/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUser.java b/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUser.java index 1c63d96..6253802 100644 --- a/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUser.java +++ b/src/main/java/com/eactive/apim/portal/portaluser/entity/PortalUser.java @@ -5,13 +5,14 @@ import com.eactive.apim.portal.portalorg.entity.PortalOrg; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.ApprovalStatus; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode; import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.UserStatus; -import java.io.Serializable; +import com.eactive.ext.kjb.safedb.SafeDBMNotRnnoConverter; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Comment; import org.hibernate.annotations.GenericGenerator; import javax.persistence.*; +import java.io.Serializable; import java.time.LocalDateTime; @Data @@ -28,8 +29,9 @@ public class PortalUser extends Auditable implements Serializable, com.eactive.e @GeneratedValue(generator = "uuid-gen", strategy = GenerationType.IDENTITY) private String id; - @Column(name = "login_id", length = 32, nullable = false) + @Column(name = "login_id", length = 200, nullable = false) @Comment("로그인ID") + @Convert(converter = SafeDBMNotRnnoConverter.class) private String loginId; @Column(name = "password_hash", length = 60) @@ -40,16 +42,19 @@ public class PortalUser extends Auditable implements Serializable, com.eactive.e @Comment("사용자이름") private String userName; - @Column(name = "phone_number", length = 12) + @Column(name = "phone_number", length = 50) @Comment("전화번호") + @Convert(converter = SafeDBMNotRnnoConverter.class) private String phoneNumber; - @Column(name = "mobile_number", length = 20) + @Column(name = "mobile_number", length = 50) @Comment("휴대폰 번호") + @Convert(converter = SafeDBMNotRnnoConverter.class) private String mobileNumber; - @Column(name = "email_addr", length = 30) + @Column(name = "email_addr", length = 200) @Comment("이메일주소") + @Convert(converter = SafeDBMNotRnnoConverter.class) private String emailAddr; @ManyToOne(fetch = FetchType.EAGER) diff --git a/src/main/java/com/eactive/ext/kjb/safedb/SafeDBBroker.java b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBBroker.java new file mode 100644 index 0000000..187d450 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBBroker.java @@ -0,0 +1,19 @@ +package com.eactive.ext.kjb.safedb; + +import java.lang.reflect.InvocationTargetException; + +public abstract class SafeDBBroker extends SafeDBFaker{ + private Object instance; + + protected void initialize() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Class clazz = Class.forName("com.initech.safedb.SimpleSafeDB"); + instance = clazz.getMethod("getInstance").invoke(null); + } + + protected Object methodInvoke(String methodName, Class[] paramTypes, Object[] params) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + if (instance == null) { + throw new IllegalStateException("SafeDBBroker is not initialized."); + } + return instance.getClass().getMethod(methodName, paramTypes).invoke(instance, params); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/safedb/SafeDBColumns.java b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBColumns.java new file mode 100644 index 0000000..a4d6b73 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBColumns.java @@ -0,0 +1,51 @@ +package com.eactive.ext.kjb.safedb; + +public enum SafeDBColumns { + RNNO("RNNO"), + PSWD("PSWD"), + NOT_RNNO("NOT_RNNO"),; + + + private final String value; + + SafeDBColumns (String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static boolean contains(String test) { + for (SafeDBColumns c : SafeDBColumns.values()) { + if (c.name().equals(test)) { + return true; + } + } + return false; + } + + public static boolean isEqualsIgnoreCase(SafeDBColumns column, String test) { + if (column == null || test == null) { + return false; + } + return column.getValue().equalsIgnoreCase(test); + } + + public static SafeDBColumns fromString(String text) { + for (SafeDBColumns b : SafeDBColumns.values()) { + if (b.name().equalsIgnoreCase(text)) { + return b; + } + } + throw new IllegalArgumentException("No constant with text " + text + " found"); + } + + + public boolean isEqualsIgnoreCase(String test) { + if (test == null) { + return false; + } + return this.value.equalsIgnoreCase(test); + } +} diff --git a/src/main/java/com/eactive/ext/kjb/safedb/SafeDBFaker.java b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBFaker.java new file mode 100644 index 0000000..b174152 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBFaker.java @@ -0,0 +1,73 @@ +package com.eactive.ext.kjb.safedb; + +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; +import java.security.MessageDigest; + +/** + * SafeDBFaker provides a fake encryption method for testing purposes when the real SafeDB library is not available. + */ +public abstract class SafeDBFaker { + private static final String AES_256_KEY = "1234567890123456" + "1234567890123456"; + + + public String fakeEncrypt(String userId, String table, String column, String data) { + if (SafeDBColumns.PSWD.isEqualsIgnoreCase(column)) { + // SHA256 + Base64 인코딩된 가짜 비밀번호 반환 + return sha256Base64(data); + } else { + // AES256 암호화된 가짜 데이터 반환 + return aes256Encrypt(data); + } + } + + public String fakeDecrypt(String userId, String table, String column, String data) { + if (SafeDBColumns.PSWD.isEqualsIgnoreCase(column)) { + // 비밀번호는 복호화하지 않음 + throw new UnsupportedOperationException("Password decryption is not supported."); + } else { + // AES256 복호화된 가짜 데이터 반환 + return aes256Decrypt(data); + } + } + + + private String sha256Base64(String data) { + try { + MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256"); + + // JVM 옵션을 따라가기 위해 캐릭터셋을 명시 하지 않음. + byte[] hash = digest.digest(data.getBytes()); + + return java.util.Base64.getEncoder().encodeToString(hash); + } catch (Exception e) { + throw new RuntimeException("Error generating SHA-256 hash", e); + } + } + + // AES256 암호화 + private String aes256Encrypt(String data) { + try { + Cipher cipher = javax.crypto.Cipher.getInstance("AES"); + SecretKeySpec keySpec = new javax.crypto.spec.SecretKeySpec(AES_256_KEY.getBytes(), "AES"); + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, keySpec); + byte[] encrypted = cipher.doFinal(data.getBytes()); + return java.util.Base64.getEncoder().encodeToString(encrypted); + } catch (Exception e) { + throw new RuntimeException("Error during AES encryption", e); + } + } + + private String aes256Decrypt(String encryptedData) { + try { + Cipher cipher = javax.crypto.Cipher.getInstance("AES"); + SecretKeySpec keySpec = new javax.crypto.spec.SecretKeySpec(AES_256_KEY.getBytes(), "AES"); + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, keySpec); + byte[] decodedBytes = java.util.Base64.getDecoder().decode(encryptedData); + byte[] decrypted = cipher.doFinal(decodedBytes); + return new String(decrypted); + } catch (Exception e) { + throw new RuntimeException("Error during AES decryption", e); + } + } +} diff --git a/src/main/java/com/eactive/ext/kjb/safedb/SafeDBMNotRnnoConverter.java b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBMNotRnnoConverter.java new file mode 100644 index 0000000..d04f77f --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBMNotRnnoConverter.java @@ -0,0 +1,81 @@ +package com.eactive.ext.kjb.safedb; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter +@Slf4j +public class SafeDBMNotRnnoConverter implements AttributeConverter { + /** + * 암호화 + * @param attribute the entity attribute value to be converted + * @return + */ + @Override + public String convertToDatabaseColumn(String attribute) { + log.debug("DB 암호화 #1 - {} / {}", attribute, SafeDBColumns.NOT_RNNO); + + String originalAttribute = attribute; + if (StringUtils.isNotEmpty(attribute)) { + SafeDBWrapper safeDBWrapper = SafeDBWrapper.getInstance(); + attribute = safeDBWrapper.encryptNotRnno(attribute); + } + + log.debug("DB 암호화 #2 : {} -> {}", originalAttribute, attribute); + + return attribute; + } + + /** + * 복호화 + * @param dbData the data from the database column to be + * converted + * @return + */ + @Override + public String convertToEntityAttribute(String dbData) { + log.debug("DB 복호화 #1 - {} / {}", dbData, SafeDBColumns.NOT_RNNO); + + String dbDataOriginal = dbData; + if (StringUtils.isNotEmpty(dbData)) { + if (this.isEncrypted(dbData)) { + SafeDBWrapper safeDBWrapper = SafeDBWrapper.getInstance(); + dbData = safeDBWrapper.decryptNotRnno(dbData); + } else { + log.debug("DB 복호화 경고: Base64 형식이 아님. 복호화하지 않고 원본 반환. dbData={}", dbData); + } + } + + log.debug("DB 복호화 #2 : {} -> {}", dbDataOriginal, dbData); + + return dbData; + } + + /** + * 암호화 여부 확인(base64 인코딩 여부로 판단, 또는 0-9, - 로 구성된 경우 암호화 되지 않은 걸로 판단(연락처)) + * @param data + * @return + */ + private boolean isEncrypted(String data) { + if (StringUtils.isEmpty(data)) { + return false; + } + + data = data.trim(); + + // Base64 형식 체크 + if (SafeDBWrapper.isBase64(data)) { + return true; + } + + // 숫자, - 로만 구성된 경우 암호화 되지 않은 걸로 판단 (ex: 연락처) + if (data.matches("^[0-9-]+$")) { + return false; + } + + return false; + } +} diff --git a/src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapper.java b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapper.java new file mode 100644 index 0000000..ea6530c --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapper.java @@ -0,0 +1,91 @@ +package com.eactive.ext.kjb.safedb; + +public class SafeDBWrapper extends SafeDBBroker { + private static SafeDBWrapper instance; + private boolean isRealSafeDB = false; + + + public static SafeDBWrapper getInstance() { + if ( instance == null ) { + synchronized ( SafeDBWrapper.class ) { + if ( instance == null ) { + instance = new SafeDBWrapper(); + } + } + } + return instance; + } + + public static boolean isBase64(String str) { + if (str == null || str.isEmpty()) { + return false; + } + String base64Pattern = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"; + return str.matches(base64Pattern); + } + + + private SafeDBWrapper() throws SafeDBWrapperException { + synchronized ( SafeDBWrapper.class ) { + // "com.initech.safedb.SimpleSafeDB" 클래스 존재 여부 확인 + try { + Class.forName("com.initech.safedb.SimpleSafeDB"); + isRealSafeDB = true; // 클래스가 존재하면 true로 설정 + } catch (ClassNotFoundException e) { + isRealSafeDB = false; // 클래스가 존재하지 않으면 false로 설정 + } catch (Exception e) { + throw new SafeDBWrapperException(("[SafeDBWrapper] Classloader error: " + e.getMessage()), e); + } + + if (isRealSafeDB) { + try { + super.initialize(); + } catch (Exception e) { + throw new SafeDBWrapperException("[SafeDBWrapper] Initialization error: " + e.getMessage(), e); + } + } + } + } + + + public String encrypt(String userId, String table, String column, String data) throws SafeDBWrapperSdkException { + if (isRealSafeDB) { + try { + return (String) super.methodInvoke("encrypt", + new Class[]{String.class, String.class, String.class, String.class}, + new Object[]{userId, table, column, data}); + } catch (Exception e) { + throw new SafeDBWrapperSdkException("[SafeDBWrapper] Encrypt error: " + e.getMessage(), e); + } + } else { + return super.fakeEncrypt(userId, table, column, data); + } + } + + public String encryptNotRnno(String data) throws SafeDBWrapperSdkException { + return this.encrypt("SAFEDB", "SAFEDB.POLICY", "NOT_RNNO", data); + } + + + public String decrypt(String userId, String table, String column, String data) throws SafeDBWrapperSdkException { + if (isRealSafeDB) { + try { + return (String) super.methodInvoke("decrypt", + new Class[]{String.class, String.class, String.class, String.class}, + new Object[]{userId, table, column, data}); + } catch (Exception e) { + throw new SafeDBWrapperException("[SafeDBWrapper] Decrypt error: " + e.getMessage(), e); + } + } else { + return super.fakeDecrypt(userId, table, column, data); + } + } + + public String decryptNotRnno(String data) throws SafeDBWrapperSdkException { + return this.decrypt("SAFEDB", "SAFEDB.POLICY", "NOT_RNNO", data); + } + + public boolean isRealSafeDB() { + return isRealSafeDB; + } +} diff --git a/src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapperException.java b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapperException.java new file mode 100644 index 0000000..e28c846 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapperException.java @@ -0,0 +1,46 @@ +package com.eactive.ext.kjb.safedb; + +public class SafeDBWrapperException extends RuntimeException { + private Exception cause; + private int errcode; + + + public SafeDBWrapperException() { + } + + + public SafeDBWrapperException(String message) { + super(message); + } + + public SafeDBWrapperException(String message, Throwable cause) { + super(message, cause); + } + + public SafeDBWrapperException(String message, Exception cause) { + super(message); + this.cause = cause; + } + + public SafeDBWrapperException(int errcode, String message) { + super(message); + this.errcode = errcode; + } + + public SafeDBWrapperException(String errcode, String message) { + super(message); + this.errcode = new Integer(errcode); + } + + public Throwable getCause() { + return this.cause; + } + + public String getMessage() { + return this.cause != null ? super.getMessage() + "\n caused by [ " + this.cause.getMessage() + " ]" : super.getMessage(); + } + + public int getErrorCode() { + return this.errcode; + } +} diff --git a/src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapperSdkException.java b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapperSdkException.java new file mode 100644 index 0000000..750da82 --- /dev/null +++ b/src/main/java/com/eactive/ext/kjb/safedb/SafeDBWrapperSdkException.java @@ -0,0 +1,18 @@ +package com.eactive.ext.kjb.safedb; + +public class SafeDBWrapperSdkException extends SafeDBWrapperException { + public SafeDBWrapperSdkException(Exception ex) { + super(ex.getMessage(), ex); + } + + public SafeDBWrapperSdkException(String message) { + super(message); + } + + public SafeDBWrapperSdkException(String message, Exception ex) { + super(message, ex); + } + + public SafeDBWrapperSdkException() { + } +} diff --git a/src/test/java/com/eactive/ext/kjb/safedb/test/SafeDbWrapperTest.java b/src/test/java/com/eactive/ext/kjb/safedb/test/SafeDbWrapperTest.java new file mode 100644 index 0000000..37509e8 --- /dev/null +++ b/src/test/java/com/eactive/ext/kjb/safedb/test/SafeDbWrapperTest.java @@ -0,0 +1,95 @@ +package com.eactive.ext.kjb.safedb.test; + +import com.eactive.ext.kjb.safedb.SafeDBWrapper; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SafeDbWrapperTest { + + @Test + public void faceSafeDBTest() { + log("FakeSafeDBWrapper Length Validation Start"); + + SafeDBWrapper wrapper; + try { + wrapper = SafeDBWrapper.getInstance(); + } catch (Exception e) { + // classloader 에러인지 initialization 에러인지 구분 + if (e.getMessage().contains("Classloader error")) { + throw new RuntimeException("SafeDBWrapper classloader error: " + e.getMessage(), e); + } else { + // initialization 에러(로컬에서는 동작하지 않는다 = 정상 처리) + log("SafeDBWrapper initialization error (expected in local environment): " + e.getMessage()); + return; + } + } + + if (wrapper.isRealSafeDB()) { + log("This test is for FakeSafeDBWrapper only. Skipping test."); + return; + } + + + for (int len = 1; len <= 255; len++) { + String originalData = repeat("A", len); // JDK8 호환 반복 문자열 + try { + // 암호화 (Base64 인코딩 결과 가정) + String encryptedData = wrapper.encrypt("userId", "tableName", "columnName", originalData); + + // 예상 암호문 길이 계산 + int expectedLen = getExpectedCipherLength(len, true); + + // 복호화 + String decryptedData = wrapper.decrypt("userId", "tableName", "columnName", encryptedData); + assertEquals(originalData, decryptedData, "Decrypted data does not match original at plain length=" + len); + + // 로그 출력 + log(String.format("Plain=%3d, Encrypted=%3d, Expected=%3d %s", + len, encryptedData.length(), expectedLen, + (encryptedData.length() == expectedLen ? "✔" : "❌"))); + + // 검증 + // "Length mismatch at plain length=" + assertEquals(len, expectedLen, encryptedData.length()); + + + // 암복호화 로그 출력 + log(String.format("Original: %s", originalData) + + String.format("\nEncrypted: %s", encryptedData) + + String.format("\nDecrypted: %s", decryptedData) + "\n"); + + + } catch (Exception e) { + throw new RuntimeException("Error at plain length=" + len, e); + } + } + } + + /** + * 암호문 예상 길이 계산 (AES/SEED/ARIA CBC 기준, PKCS5Padding) + */ + private int getExpectedCipherLength(int plainLength, boolean base64Encoding) { + int blockSize = 16; + int paddedLength = ((plainLength / blockSize) + 1) * blockSize; + + if (base64Encoding) { + return (int) (Math.ceil(paddedLength / 3.0) * 4); + } else { + return paddedLength; + } + } + + /** JDK8에서 문자열 반복 (String.repeat 대체) */ + private String repeat(String s, int count) { + StringBuilder sb = new StringBuilder(s.length() * count); + for (int i = 0; i < count; i++) { + sb.append(s); + } + return sb.toString(); + } + + private void log(String message) { + System.out.println(message); + } +}