본문 바로가기
언어 공부/Java

[Java] Jasypt 알아보기

by 희조당 2023. 10. 21.

🙋 들어가며

프로젝트를 진행할 때 보안에 예민해서 노출시키지 않는 값들이 있습니다.

여러 방법으로 노출시키지 않는 방법이 있지만 그중에 Jasypt라는 라이브러리가 존재합니다.

오늘은 어떻게 사용하는지 가볍게 알아보겠습니다. 😋


🔐 Jasypt란?

Jasypt는 Java Simplified Encryption의 약자입니다.

공식 문서에 따르면 다음과 같이 설명되어 있습니다.

Java library which allows the developer to add basic encryption capabilities to his/her projects with minimum effort, and without the need of having deep knowledge on how cryptography works.

개발자가 암호학에 대한 지식이 없어도 기본적인 암호화 기능을 사용할 수 있게 하는 라이브러리입니다.

오래된 라이브러리이지만 설명 그대로 쉽게 사용할 수 있어 지금도 많이 사용합니다.

😎 의존성 추가하기

우선 라이브러리에 대한 의존성을 추가해야 합니다.

// gradle
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4'

starter가 붙어서 많은 의존성이 추가될 것 같지만 작고 귀엽습니다. 😋

👻 기본 사용법

Jasypt는 다양한 암호화 툴을 제공합니다.

String을 포함한 Decimal, Integer, Byte 이렇게 총 4가지를 지원합니다.

다양한 Encrytor

 

우리는 yml 파일에 작성되는 값들을 숨겨야하기 때문에 문자열 암호화 도구를 사용해 보겠습니다.

이름 그대로 가장 기본적인 암호화 도구로 테스트를 작성해 보았습니다.

@Test
void encryptString_Success_EqualsWithDecrypted() {
    // given
    String password = "password";
    String target = "hejow";
    PBEStringCleanablePasswordEncryptor encryptor = initEncryptor(password);

    // when
    String encrypted = encryptor.encrypt(target);

    // then
    assertEquals(target, encryptor.decrypt(encrypted)); // true
}

private PBEStringCleanablePasswordEncryptor initEncryptor(String password) {
    StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
    encryptor.setPassword(password);
    encryptor.setAlgorithm("PBEWithMD5AndDES");

    return encryptor;
}

 

여기까지 테스트는 이상이 없지만 생성되는 값이 항상 똑같지 않습니다.

따라서 다음 테스트가 정상적으로 실행됩니다.

@Test
void isEncryptedAlwaysSame_Fail_ByDifferentSalt() {
    // given
    String target = "john";
    PBEStringCleanablePasswordEncryptor encryptor = initEncryptor(password);

    // when
    String encrypted = encryptor.encrypt(target);

    // then
    assertAll(
            () -> assertNotEquals(encrypted, encryptor.encrypt(target)),
            () -> assertNotEquals(encrypted, encryptor.encrypt(target)),
            () -> assertNotEquals(encrypted, encryptor.encrypt(target))
    );
}

🧂 Salt?

앞선 테스트의 이름을 유심히 보면 다른 Salt 때문에 실패했다고 명시했습니다.

여기서 Salt란, 암호학에서 해시 처리할 때 사용되는 추가적인 랜덤한 값을 의미합니다.

 

Salt를 생성하는 generator를 encryptor에 넣어주면 항상 같은 값을 생성하게 됩니다.

@Test
void sameEncrypted_Success_WithFixedSaltGenerator() {
    // given
    String target = "moon";
    String salt = "fixedSalt";
    StandardPBEStringEncryptor encryptor = initEncryptor(password);
    encryptor.setSaltGenerator(new StringFixedSaltGenerator(salt));

    // when
    String encrypted = encryptor.encrypt(target);

    // then
    assertAll(
            () -> assertEquals(encrypted, encryptor.encrypt(target)),
            () -> assertEquals(encrypted, encryptor.encrypt(target)),
            () -> assertEquals(encrypted, encryptor.encrypt(target))
    );
}

🙈 Property 암호화하기

여기까지 인트로였고, 이제 원래 목적에 맞게 사용하는 법을 안내드리겠습니다.

🍥 Configuration 작성

다음은 많은 블로그에서 작성된 코드입니다.

개인적으로 “개발자가 작성하는 코드의 의미를 꼭 알아야 한다”라고 생각합니다. (근데 많은 블로그는 안 그런 것 같습니다.)

 

각 옵션에 대해서 살펴보면서 어떤 점이 잘못되었는지 살펴보겠습니다.

@Configuration
@EnableEncryptableProperties
public class JasyptConfig {

    @Value("${jasypt.encryptor.password}")
    private String password;

    @Bean("jasyptStringEncryptor")
    public StringEncryptor stringEncryptor() {
        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();
        config.setPassword(password);
        config.setAlgorithm("PBEWithMD5AndDES");
        config.setKeyObtentionIterations("1000");
        config.setPoolSize("1");
        config.setProviderName("SunJCE");
        config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
        config.setIvGeneratorClassName("org.jasypt.iv.NoIvGenerator");
        config.setStringOutputType("base64");
        encryptor.setConfig(config);
        return encryptor;
    }
}

🗝️ PooledPBEStringEncryptor

Jasypt가 제공하는 Encryptor는 StandardPBE 타입과 PooledPBE 타입이 존재합니다.

두 타입이 제공하는 기능은 정확하게 동일하나 딱 한 가지 차이점이 존재합니다.

 

다음은 PooledPBEStringEncryptor의 Javadoc입니다.

 

PooledPBEStandardPBE을 PoolSize만큼 가지고 있다가 요청이 오면 라운드-로빈 방식으로 처리합니다.

쉽게 설명하면 커넥션을 미리 생성해 둔 커넥션 풀과 같은 구조입니다.

🔑 StandardPBEStringEncryptor

이 Encrytor를 생성하면 다음과 같이 초기화됩니다.

public StandardPBEStringEncryptor() {
      super();
      this.byteEncryptor = new StandardPBEByteEncryptor();
      this.base64 = new Base64();
}

 

기본적으로 Base64 인코딩을 사용한다는 것을 알 수 있고 byteEncrytor도 살펴보겠습니다.

StandardPBEByteEncryptor는 다음과 같이 초기화됩니다.

public StandardPBEByteEncryptor() {
    super();
}

 

내부적으로 어떤 값 초기화하지는 않지만 필드를 살펴보면 이미 초기값들이 선언되어 있습니다.

public static final String DEFAULT_ALGORITHM = "PBEWithMD5AndDES";
public static final int DEFAULT_KEY_OBTENTION_ITERATIONS = 1000;
public static final int DEFAULT_SALT_SIZE_BYTES = 8;
public static final int DEFAULT_IV_SIZE_BYTES = 16;

private String algorithm = DEFAULT_ALGORITHM;
private String providerName = null;
private Provider provider = null;
private char[] password = null;

private int keyObtentionIterations = DEFAULT_KEY_OBTENTION_ITERATIONS;
private SaltGenerator saltGenerator = null;
private int saltSizeBytes = DEFAULT_SALT_SIZE_BYTES;

private IvGenerator ivGenerator = null;
private int ivSizeBytes = DEFAULT_IV_SIZE_BYTES;

private PBEConfig config = null;

// e.t.c ...

 

이 초기값들은 기존 Configuration에서 추가한 설정과 중복되는 부분이 많습니다.

그리고, 개인적으로 불필요한 리플렉션을 사용하는 것을 지양합니다.

@Bean("jasyptStringEncryptor")
public StringEncryptor stringEncryptor() {
    // ... 

    // 중복!!
    config.setAlgorithm("PBEWithMD5AndDES");
    config.setKeyObtentionIterations("1000");
    config.setStringOutputType("base64");

    // 의미 없음
    config.setPoolSize("1");
    config.setProviderName("SunJCE");

    // 불필요한 리플렉션
    config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
    config.setIvGeneratorClassName("org.jasypt.iv.NoIvGenerator");

    return ...;
}

😜 개선된 Configuration

앞서 살펴봤던 문제점을 모두 반영해서 수정하면 다음 설정만 있어도 충분합니다.

특히, 우리는 초기화될 때만 사용하므로 StandardPBEStringEncryptor사용해도 무방할 것 같습니다.

@Configuration
@EnableEncryptableProperties
public class JasyptConfig {

    @Value("${jasypt.encryptor.password}")
    private String password;

    @Bean("jasyptStringEncryptor")
    public StringEncryptor stringEncryptor() {
        StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();

        encryptor.setPassword(password);
        encryptor.setSaltGenerator(new RandomSaltGenerator());
        encryptor.setIvGenerator(new NoIvGenerator());

        return encryptor;
    }
}

🔫 적용하기

다음과 같이 yml 파일이 작성되어 있다고 가정해보겠습니다.

딱 봐도 너무나도 중요한 설정으로 보입니다. 당연히 노출시키면 안되겠죠?

중요한설정:
  진짜중요함:
    API키: password

 

우리만 사용할 키를 정한 다음, 암호화 사이트를 들어갑니다. 

다음 부분만 주의해서 선택한 다음 암호화합니다.

 

다음과 같이 yml 파일을 작성하면 깔끔하게 암호화가 되었습니다 😋😋

중요한설정:
  진짜중요함:
    API키: ENC(Fia8u6rJmpYLgVqIe8G6haCE/U3/zfogeXlek+HY0zetfCDlj+VOeVcT9DJ8S9KN)

😋 정리

Jasypt는 간편하게 암호화를 지원하는 하나의 라이브러리입니다.

일반적으로 스프링의 환경 변수를 숨기기 위해서 많이 사용합니다.

@Configuration을 통해서 등록할 수 있고 간단하게 설정해 주면 됩니다.

(사실 코드가 많은 블로그 글들이 싫어서 작성했습니다.)


-Reference:

http://www.jasypt.org/

 

😋 지극히 개인적인 블로그지만 댓글과 조언은 제 성장에 도움이 됩니다 😋

댓글