Java AES256 Sample 코드.

|

AES 란?

Advanced Encryption Standard를 줄인 말이다. 한국어로 번역하면 ‘고급 암호화 표준’이다. 대칭키를 쓰는 블럭 암호이다. 높은 안전성과 속도로 인해 인기를 얻어 전 세계적으로 많이 사용되고 있다.

특징

대칭형, 블럭 암호화 알고리즘이다.
대칭형 암호화 알고리즘 중 가장 유명하다.
암호화 키는 128, 192, 256의 세 가지 중 하나가 될 수 있으며, 각각 AES-128, AES-192, AES-256으로 불린다.

[2026년 추가] 몇 년 지나 다시 읽어보니 아래 예제에 보안상 문제가 있어서 짚고 넘어간다. 원본 코드는 “당시 작성한 그대로” 남겨두고, 아래쪽에 고친 버전을 추가했다.

  • IV(초기화 벡터)를 고정값으로 쓰고 있음: IV = secretKey.substring(0, 16) — 매번 같은 IV를 재사용한다. IV의 목적 자체가 “같은 평문을 여러 번 암호화해도 매번 다른 암호문이 나오게” 하는 것인데, 고정 IV를 쓰면 이 목적이 완전히 무너진다. 같은 값을 두 번 암호화하면 결과도 항상 똑같이 나오고, 이는 공격자가 패턴을 분석할 실마리를 준다.
  • AES/CBC/PKCS5Padding 조합은 패딩 오라클 공격(Padding Oracle Attack)에 취약할 수 있음: CBC 모드는 인증(무결성 검증)을 해주지 않아서, 암호문이 변조돼도 복호화 시 에러 메시지 차이로 원문을 유추당할 위험이 있다. 요즘은 암호화와 동시에 인증까지 해주는 AES/GCM 모드를 기본으로 권장한다.
  • 비밀키를 소스코드에 하드코딩: 이건 AES256 자체의 문제는 아니지만, 실무에서는 키를 소스에 두지 않고 환경변수나 AWS KMS/Vault 같은 시크릿 매니저로 분리해야 한다.

AES256Cipher.java (예전 예제 - 위에서 지적한 문제가 있음)

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidAlgorithmParameterException;

import org.apache.commons.codec.binary.Base64;

public class AES256Cipher {

    private static volatile AES256Cipher INSTANCE;

    final static String secretKey = "jmlim12345bbbbbaaaaa123456789066"; //32bit
    static String IV = ""; //16bit

    public static AES256Cipher getInstance() {
        if (INSTANCE == null) {
            synchronized (AES256Cipher.class) {
                if (INSTANCE == null)
                    INSTANCE = new AES256Cipher();
            }
        }
        return INSTANCE;
    }

    private AES256Cipher() {
        IV = secretKey.substring(0, 16);
    }

    //암호화
    public static String AES_Encode(String str) throws java.io.UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
        byte[] keyData = secretKey.getBytes();

        SecretKey secureKey = new SecretKeySpec(keyData, "AES");

        Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
        c.init(Cipher.ENCRYPT_MODE, secureKey, new IvParameterSpec(IV.getBytes()));

        byte[] encrypted = c.doFinal(str.getBytes("UTF-8"));
        String enStr = new String(Base64.encodeBase64(encrypted));

        return enStr;
    }

    //복호화
    public static String AES_Decode(String str) throws java.io.UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
        byte[] keyData = secretKey.getBytes();
        SecretKey secureKey = new SecretKeySpec(keyData, "AES");
        Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
        c.init(Cipher.DECRYPT_MODE, secureKey, new IvParameterSpec(IV.getBytes("UTF-8")));

        byte[] byteStr = Base64.decodeBase64(str.getBytes());

        return new String(c.doFinal(byteStr), "UTF-8");
    }
}

AES256CipherTest.java


import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import org.junit.Test;

public class AES256CipherTest {
    String id = "hackerljm";
    String custrnmNo = "1234";
    String custNm = "정묵테스트";

    @Test
    public void encDesTest() throws InvalidKeyException, UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
        AES256Cipher a256 = AES256Cipher.getInstance();

        String enId = a256.AES_Encode(id);
        String enYyyymmdd = a256.AES_Encode(custrnmNo);
        String enCustNm = a256.AES_Encode(custNm);

        String desId = a256.AES_Decode(enId);
        String desYyyymmdd = a256.AES_Decode(enYyyymmdd);
        String desCustNm = a256.AES_Decode(enCustNm);

        assertThat(id, is(desId));
        assertThat(custrnmNo, is(desYyyymmdd));
        assertThat(custNm, is(desCustNm));
    }
}

AES256GcmCipher.java (수정된 예제 - GCM 모드 + 매 암호화마다 랜덤 IV)

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

public class AES256GcmCipher {

    private static final int GCM_TAG_LENGTH_BIT = 128;
    private static final int GCM_IV_LENGTH_BYTE = 12; // GCM 권장 IV 길이

    private final SecretKey secretKey;

    // 비밀키는 생성자로 주입받는다 - 소스에 하드코딩하지 않고 환경변수/시크릿 매니저에서 가져온 값을 넘겨줄 것
    public AES256GcmCipher(byte[] key32Byte) {
        this.secretKey = new SecretKeySpec(key32Byte, "AES");
    }

    // 암호화 - 호출할 때마다 새 IV를 생성하고, IV를 암호문 앞에 붙여서 함께 반환
    public String encrypt(String plainText) throws Exception {
        byte[] iv = new byte[GCM_IV_LENGTH_BYTE];
        new SecureRandom().nextBytes(iv); // 매번 랜덤 IV - 이게 원본 예제의 가장 큰 문제를 고치는 부분

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_LENGTH_BIT, iv));

        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));

        // IV(12바이트) + 암호문을 이어붙여 하나의 값으로 저장 -> 복호화 시 앞 12바이트를 다시 IV로 꺼내 씀
        byte[] combined = new byte[iv.length + encrypted.length];
        System.arraycopy(iv, 0, combined, 0, iv.length);
        System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);

        return Base64.getEncoder().encodeToString(combined);
    }

    // 복호화 - 앞 12바이트를 IV로 분리한 뒤 나머지를 복호화
    public String decrypt(String cipherTextBase64) throws Exception {
        byte[] combined = Base64.getDecoder().decode(cipherTextBase64);

        byte[] iv = new byte[GCM_IV_LENGTH_BYTE];
        byte[] encrypted = new byte[combined.length - GCM_IV_LENGTH_BYTE];
        System.arraycopy(combined, 0, iv, 0, iv.length);
        System.arraycopy(combined, iv.length, encrypted, 0, encrypted.length);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_LENGTH_BIT, iv));

        return new String(cipher.doFinal(encrypted), "UTF-8");
    }
}
// 사용 예 - 키는 실제로는 환경변수/시크릿 매니저에서 로드
byte[] key = System.getenv("AES_SECRET_KEY").getBytes("UTF-8"); // 32byte(256bit)
AES256GcmCipher cipher = new AES256GcmCipher(key);

String encrypted = cipher.encrypt("정묵테스트");
String decrypted = cipher.decrypt(encrypted);

GCM은 IV를 암호문마다 다르게 쓰는 대신, 그 IV 자체는 비밀로 감출 필요가 없다(공개돼도 안전하도록 설계됨) — 그래서 위처럼 IV를 암호문 앞에 그냥 붙여서 저장해도 된다. 다만 같은 키로 같은 IV를 두 번 쓰는 것만큼은 절대 피해야 하며, SecureRandom으로 매번 새로 생성하는 것이 이를 보장하는 가장 간단한 방법이다.

참고:

  • https://dukeom.wordpress.com/2013/01/08/aes256-%EC%95%94%ED%98%B8%ED%99%94-java-%EC%83%98%ED%94%8C/
  • https://namu.wiki/w/AES
  • OWASP Cryptographic Storage Cheat Sheet

Comments