Problem

  • 현재 MySQLMariaDB의 지원하는 암호화 알고리즘 차이가 있음
  • AES_ENCRYPTMySQLMariaDB 동일하게 존재함 (현재 최신버전 기준, 추후에 지원 여부가 변경될 수 있으니 버전 확인 필요)
    • MySQL 8.01
    • MariaDB 10.102
  • AES_ENCRYPT 사용 방법
      INSERT INTO t VALUES (AES_ENCRYPT('text',SHA2('password',512)));
    
      -- base64로 저장하는 경우
      SELECT to_base64(AES_ENCRYPT(mobile, 'text')) FROM t;
      SELECT AES_DECRYPT(from_base64(mobile), 'text') FROM t;
    
  • block_encryption_mode
    • block_encryption_mode 옵션을 이용해 암호화 모드를 변경할 수 있다.
    • MySQLMariaDB에서 그냥 AES_ENCRYPTAES_DECRYPT를 실행할 경우 시스템 환경 변수에 기본 옵션으로 실행되기 때문에 원하는 알고리즘 및 모드인지 확인이 필요
    • MySQL
      • 다음 설명에서 볼 수 있듯이 MySQL은 다양한 알고리즘을 지원하고 있다.3 특히 CBC256의 키 길이를 지원한다는 것.

        block_encryption_mode takes a value in aes-keylen-mode format, where keylen is the key length in bits and mode is the encryption mode. The value is not case-sensitive. Permitted keylen values are 128, 192, and 256. Permitted mode values are ECB, CBC, CFB1, CFB8, CFB128, and OFB.

      • 블록모드 설정과 확인은 아래처럼 할 수 있음
          select @@session.block_encryption_mode;
          SET @@session.block_encryption_mode = 'aes-256-ecb';
        
    • MariaDB
      • System Variable Differences Between MariaDB 10.0 and MySQL 5.64 문서에도 볼 수 있듯이 AES-128-ECB128bit의 키 길이와 ECB 모드만 지원

Solution

  1. MariaDB를 사용한다면 UDF(User defined function)를 설치해서 사용
    • lib_mysqludf_aes2565과 같은 UDF를 설치해서 AES-256-CBC 지원 가능
    • AWS RDB 에서는 사용 불가능
  2. UDF를 사용하지 않고 AES-128-ECB를 사용해야 한다면,
    • 암호화를 디비에서 구현하지 않고 다른 외부 코드에서 구현하여 암호화된 데이터만 저장하는 방식으로 진행
    • 관리 불편, like 검색과 같은 기능 필요 시 고민을 해야함
  3. 암호화 키 길이를 줄이고 현재 지원하고 있는 AES-128-ECB를 사용함
    • CBC에 비해 암호화 보안이 상대적으로 취약할 수 있음
    • 그러나 잘 동작하고 사용하기도 편함
  4. kotlin으로 구현한 AES (AES-128-ECB) 예시
    • MariaDB와 동일한 코드 사용으로 관리 용이
class AESUtil {
    var iv: String?
    var keySpec: Key?

    constructor(key: String, length: Int = 32) {
        iv = key.substring(0, length)
        val keyBytes = ByteArray(length)
        val b = key.toByteArray(StandardCharsets.UTF_8)
        var len = b.size
        if (len > keyBytes.size) {
            len = keyBytes.size
        }

        System.arraycopy(b, 0, keyBytes, 0, len)
        keySpec = SecretKeySpec(keyBytes, "AES")
    }

    fun encrypt(str: String): String? {
        return try {
            val c = Cipher.getInstance("AES/ECB/PKCS5Padding")
            val key: SecretKey = SecretKeySpec(iv?.toByteArray(), "AES")

            c.init(Cipher.ENCRYPT_MODE, key)
            val encrypted = c.doFinal(str.toByteArray(StandardCharsets.UTF_8))
            String(Base64.encodeBase64(encrypted))

        } catch (e: NoSuchAlgorithmException) {
            logger.error("AES/CBC/PKCS5Padding 해당 알고리즘을 지원하지 않습니다. ('$str'를 암호화하는데 실패했습니다.)", e)
            null
        } catch (e: GeneralSecurityException) {
            logger.error("키가 잘못되었습니다. ('$str'를 암호화하는데 실패했습니다.)", e)
            null
        }
    }

    fun decrypt(str: String): String? {
        return try {
            val c = Cipher.getInstance("AES/ECB/PKCS5Padding")
            val key: SecretKey = SecretKeySpec(iv?.toByteArray(), "AES")
            c.init(Cipher.DECRYPT_MODE, key)

            val byteStr = Base64.decodeBase64(str.toByteArray())
            String(c.doFinal(byteStr), StandardCharsets.UTF_8)
        } catch (e: NoSuchAlgorithmException) {
            logger.error("AES/CBC/PKCS5Padding 해당 알고리즘을 지원하지 않습니다. ('$str'를 복호화하는데 실패했습니다.)", e)
            null
        } catch (e: GeneralSecurityException) {
            logger.error("키가 잘못되었습니다. ('$str'를 복호화하는데 실패했습니다.)", e)
            null
        }
    }

    companion object {
        var logger = LoggerFactory.getLogger(AES256Util::class.java)
    }
}

References