Skip to content

암호화를 위한 AES 분석

hong seokho edited this page Jan 29, 2024 · 1 revision

전화번호 기반의 서비스라서 회원가입 시 전화번호 인증을 받고 사용자 전화번호를 저장합니다. 현재 프로젝트는 구글 플레이스토어에 출시할 예정이라 식별할 수 있는 개인정보는 안전하게 암호화해서 저장해야 했습니다.

kisa FAQ

문제 상황

개인을 식별할 수 있는 핸드폰 번호가 암호화되어야 합니다.

해결 방안 탐구

암호화 기술을 적용해 전화번호 인증 시 휴대폰 번호를 암호화해야합니다.

단방향 암호화, 양방향 암호화, 대칭키 암호화, 공개키 암호화 방식이 있습니다.

  • 단방향 암호화 - 암호화는 가능하나 복호화는 불가능한 암호화 ex) sha256, brcypt
  • 양방향 암호화 - 암호화, 복호화가 가능 ex) AES, RSA
  • 대칭키 암호화 - 하나의 키로 암복호화 ex) AES
  • 공개키 암호화 - 비밀 키와 공개 키를 사용해 공개 키로 암호화해 비밀 키로 복호화 ex) RSA

저희는 핸드폰 번호를 복호화해 사용자에게 전달해야 하므로 양방향 암호화 방식을 택했습니다. RSA는 AES보다 긴 길이를 가지고 있고 속도가 느립니다. 또한 하나의 서버에서 하나의 키로 암복호화를 진행하기에 대칭키 암호화 방식인 AES가 적합하다고 생각했습니다.

최종적으로 AES를 택했습니다.

AES란? (Advanced Encryption Standard)

우선 AES를 이해할 필요가 있었습니다.

DES는 키 길이가 짧아서 다양한 공격에 의해 무효화되고 있어서 그것을 대체할 것이 필요했습니다. DES(Data Encrpyion Standard)를 대체할 암호화 알고리즘을 미국 표준 기술 연구소에서 공개 경쟁에 붙여서 벨기에 암호학자 2명이 개발한 rijndael 암호 알고리즘이 AES가 되었습니다. AES는 128비트의 블록을 기반으로 암호화하는 블록 기반 암호화 알고리즘입니다.

주요 특징은 SP network를 이용합니다. 1을 6으로 단순히 바꾸는 것만이 아닌 입력을 바꾸고 그 입력들을 섞어버립니다.

예를 들면, 1 -> 6, 2 -> 3, 5 -> 7일 때 SP network는 125를 넣으면 763과 같이 입력도 바꾸고 바꾼 값들도 섞어버립니다.

AES는 128, 192, 256비트의 키가 필요합니다.

1

키의 크기에 따라 10, 12, 14라운드가 반복됩니다.

키가 길어지면 라운드를 더 반복해 보안성을 강화합니다.

GCM (Galois/Counter Mode)

GCM은 AES의 동작 모드 중 하나입니다. GCM은 인증 태그를 활용하여 무결성까지 챙길 수 있습니다.

GCM의 동작 방식을 살펴보겠습니다.

GCM은 권장 값인 12 바이트 크기의 IV(Initialize Vector)가 필요합니다. 이는 GCM의 계산에 필요합니다. 또한 무작위로 생성해야합니다.(매번 다른 암호화 결과를 도출하기 위함) gcm iv recomend length

인증 태그 생성

3

IV, 평문을 AES로 암호화한 값, 인증태그가 최종 결과가 됩니다.

만약 누군가 내 암호화된 데이터를 변조하면 인증 태그로부터 위변조 여부를 알 수 있습니다. 인증 태그 생성에 GHash를 적용한 이유가 이러한 위변조를 탐색할 수 있게 하기 위함입니다. 인증 태그를 추출해 받은 데이터로 인증 태그를 재생성 시 일치 확인을 통해 데이터의 위변조 유무를 알 수 있습니다.

구현

public final class AESEncryptionManager {

    private static final String ENCRYPT_ALGORITHM = "AES/GCM/NoPadding";  //블록 암호화 동작 방식 지정
    private static final int TAG_LENGTH_BIT = 128;
    private static final int IV_LENGTH_BYTE = 12;
    private static final Charset UTF_8 = StandardCharsets.UTF_8;
    private static final SecureRandom secureRandom = new SecureRandom();

    private final SecretKey key;

    public AESEncryptionManager(AESKeyProperties properties) { //String 키 주입
        byte[] decodedKey = Decoders.BASE64.decode(properties.secretKey());
        this.key = new SecretKeySpec(decodedKey, 0, decodedKey.length, "AES");
    }

    public byte[] encryptWithPrefixIV(byte[] pText) throws EncryptionException {
        byte[] iv = getRandomNonce();
        byte[] cipherText = encrypt(pText, iv);

        return ByteBuffer.allocate(iv.length + cipherText.length)
            .put(iv)
            .put(cipherText)
            .array();
    }

    private byte[] encrypt(byte[] pText, byte[] iv) {
        try {
            Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM); //암호화 해주는 인스턴스 생성
            cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); //관련 값 초기화(키 확장, IV 설정)

            return cipher.doFinal(pText);
        } catch (Exception e) {
            throw new EncryptionException(e);
        }
    }

    private byte[] getRandomNonce() {
        byte[] nonce = new byte[IV_LENGTH_BYTE];
        secureRandom.nextBytes(nonce);
        return nonce;
    }

    public String decryptWithPrefixIV(byte[] cText) throws EncryptionException {
        ByteBuffer bb = ByteBuffer.wrap(cText);

        byte[] iv = new byte[IV_LENGTH_BYTE]; //암호화된 데이터에서 IV 분리
        bb.get(iv);

        byte[] cipherText = new byte[bb.remaining()];
        bb.get(cipherText);

        return decrypt(cipherText, iv);
    }

    private String decrypt(byte[] cText, byte[] iv) {
        try {
            Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_LENGTH_BIT, iv));

            byte[] plainText = cipher.doFinal(cText);

            return new String(plainText, UTF_8);
        } catch (Exception e) {
            throw new EncryptionException(e);
        }
    }
}

byte[] iv = getRandomNonce();

이 부분에서 IV를 랜덤하게 생성합니다.

return ByteBuffer.allocate(iv.length + cipherText.length)
            .put(iv)
            .put(cipherText)
            .array();

최종 결과로 iv, cipherText이지만 cipherText는 인증태그와 암호화된 내 평문으로 구성됩니다.

        ByteBuffer bb = ByteBuffer.wrap(cText);

        byte[] iv = new byte[IV_LENGTH_BYTE];
        bb.get(iv);

        byte[] cipherText = new byte[bb.remaining()];
        bb.get(cipherText);

        return decrypt(cipherText, iv);

복호화시에는 IV와 인증태그, 암호문을 분리해서 복호화를 시도합니다.

테스트

Screenshot from 2024-01-29 10-26-01 Screenshot from 2024-01-29 10-25-46 성공적으로 개인정보 암복호화에 성공했습니다.

이번엔 암호화된 데이터의 IV 부분을 바꿨습니다. 복호화 시 오류가 발생합니다. Screenshot from 2024-01-29 10-31-35 Screenshot from 2024-01-29 10-31-49

이번엔 암호화된 데이터를 바꿨습니다. 복호화 시 오류가 발생합니다. Screenshot from 2024-01-29 10-47-58 Screenshot from 2024-01-29 10-49-52

이번에는 인증 태그를 바꿨습니다. 복호화 시 오류가 발생합니다. Screenshot from 2024-01-29 10-48-11 Screenshot from 2024-01-29 10-49-41

결과

사용자를 식별할 수 있는 개인정보인 전화번호를 데이터 베이스에 안전하게 저장할 수 있었습니다. AES 암호화는 다음 링크를 참조하여 구현했습니다.

AES 구현

참조링크