암호화 알고리즘 종류 / Java 암호화 유틸 개발 / AES256, Base64 등 암복호화 방법

단방향 해시

복호화가 불가능하여 비밀번호 해싱에 적합합니다.

해시 함수

MD5
128비트 고정 길이의 출력값을 가집니다.
보안이 취약해서 인증용으로 사용이 권장되지 않습니다.

SHA-256
데이터 무결성 검증 등에 사용되는 해시 함수입니다.
속도가 빠르기 때문에 GPU 공격에 취약하여 비밀번호 저장 용도로는 부적합합니다.

사용자가 입력한 비밀번호를 해싱 후 SHA-256 해시 앞 16자리를 잘라서
비밀번호 컬럼에 저장된 SHA-256 해시 앞 16자리와 비교하여 로그인하는 방식은
충돌 확률이 증가하고 보안성이 낮아져서 절대 권장되지 않습니다.

SHA-512
SHA-256보다 긴 해시 길이를 제공하여 충돌 저항성이 더 높지만,
해싱 속도가 빠르기 때문에 여전히 비밀번호 저장 용도로는 부적합합니다.

HMAC
비밀키를 사용하는 해시 기반 MAC 알고리즘입니다.
비밀키가 없으면 해시를 생성할 수 없어 SHA-256보다 사전 공격에 더 안전합니다.

동일 입력은 동일 출력이 되므로 전화번호 중복체크용 해시를 만들고,
별도의 컬럼 저장 후 UNIQUE 제약을 걸어 중복 가입을 방지할 수 있습니다.

비밀번호 해싱 알고리즘

bcrypt
비밀번호 저장을 위해 설계된 단방향 해싱 알고리즘입니다.
Salt가 자동으로 포함되어 동일한 비밀번호라도 다른 해시를 생성합니다.

ID를 Salt로 사용할 경우,
다른 계정 비밀번호를 복사해서 DB에 update하면 동일한 비밀번호여도 로그인이 실패됩니다.

DB에 저장된 Bcrypt 해시에서 추출한 salt + cost를 기반으로
입력한 비밀번호를 해싱 후, 결과를 비교해서 로그인 할 수 있습니다.

bcrypt는 느린 연산이라 brute-force 공격을 지연시킬 수 있습니다.
레디스를 이용하여 같은 IP로 1분 내 10회 미만 요청만 가능하도록 하면 더 안전합니다.
Cost Factor를 통해 연산 횟수를 조절하여 의도적으로 더 느리게 할 수 있습니다.

Argon2
현재 가장 권장되는 비밀번호 해싱 알고리즘입니다. 연산 시간뿐 아니라 메모리 사용량까지 증가시켜 공격 비용을 상승시킬 수 있습니다.


양방향 암호화

복호화가 가능합니다.

비밀키 암호화 기법

대칭형 암호화 알고리즘이고 비밀키로 암/복호화합니다.

블록 암호화 방식 DES, AES 등
스트림 암호화 방식 LFSR, RC4 등

이름, 폰번호, 생년월일 등 개인정보는 마이페이지에서 확인이 가능해야 하므로
AES-256-GCM 알고리즘 등으로 양방향 암호화가 필요합니다.
JPA @Convert를 이용하면 저장 시 자동 암호화, 조회 시 자동 복호화가 가능합니다.

GCM 모드는 매번 랜덤 IV를 생성하여 같은 평문도 다른 암호문을 만듭니다.

키는 KMS 등 안전한 시스템에서 관리하여 보안을 강화해야 합니다.

공개키 암호화 기법

비대칭형 암호화이고 공개키로 암호화, 개인키로 복호화합니다.

  • RSA 등

SHA512 해싱 방법

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.security.MessageDigest;

@Slf4j
@Component
public class SHA512Util {

    public String getSHA512(String raw) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-512");
        md.update(raw.getBytes());

        byte[] msgb = md.digest();

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < msgb.length; i++) {
            String tmp = Integer.toHexString(msgb[i] & 0xFF);
            while (tmp.length() < 2)
                tmp = "0" + tmp;
            sb.append(tmp.substring(tmp.length() - 2));
        }
        return sb.toString();
    }
}

위 SHA512 유틸을 통해 해싱할 수 있습니다.


AES256 암복호화 방법

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

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Key;

public class AES256Util {
    private byte[] iv;
    private byte[] sessionKey;
    private Key keySpec;

    public AES256Util(String key) {
        try {
            // 고정 IV 사용 시, 같은 평문은 동일한 암호문이 생성되므로 보안에 취약
            // 매 암호화마다 랜덤 생성되도록 변경 필요
            this.iv = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

            sessionKey = key.substring(0, 32).getBytes(StandardCharsets.UTF_8);
            SecretKeySpec secretKeySpec = new SecretKeySpec(sessionKey, "AES");

            this.keySpec = secretKeySpec;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // 암호화
    public String aesEncode(String str) {
        try {
            // AES/CBC/PKCS5Padding 대신 AES/GCM/NoPadding을 사용하면,
            // 인증을 포함해서 위변조 탐지 가능하여 더 권장됩니다.
            Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
            c.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv));

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

            return enStr;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    //복호화
    public String aesDecode(String str) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            SecretKeySpec secretKeySpec = new SecretKeySpec(sessionKey, "AES");

            cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
            byte[] cipheredText = cipher.doFinal(Base64.decodeBase64(str));
            return (new String(cipheredText, "UTF-8"));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

AES256Util 암호화 예시

AES256Util aes256Util = new AES256Util("32자리키값");
String encStr = aes256Util.aesEncode("암호화할값");

base64 인코딩 방법

import java.util.Base64;

public class Base64Util {

  public String base64Encode(String str) throws UnsupportedEncodingException {
    String result = "";
    byte[] targetBytes = str.getBytes("UTF-8");
    Base64.Encoder encoder = Base64.getEncoder();
    String encodedString = encoder.encodeToString(targetBytes);
    result = encodedString;
    return result;
  }

  public String base64Decode(String str) throws UnsupportedEncodingException {
    String result = "";
    Base64.Decoder decoder = Base64.getDecoder();
    byte[] decodedBytes2 = decoder.decode(str);
    result = new String(decodedBytes2, "UTF-8");
    return result;
  }

}

Base64는 암호화가 아닌 단순 인코딩으로, 보안 목적이 아닌 데이터 전송 및 표현을 위해 사용됩니다.

base64 인코딩 예시

Base64Util base64Util = new Base64Util();
String encStr = base64Util.base64Encode("암호화할값");