본문 바로가기

업브렐라

안전하게 비밀번호 생성하기 - HOTP

1. 문제 정의

업브렐라 보관함은 네트워크 통신이 없는 상태에서 비밀번호를 생성해야 합니다.
이에 대한 해결책으로 HOTP를 사용하여 비밀번호를 생성하기로 결정하였습니다.

2. OTP (One - Time - Password)

OTP는 무작위 번호 약속 알고리즘에 따라 (시간 혹은 카운터에 따라) 변경되는 추정할 수 없는 비밀번호 생성을 이용하는 보안 시스템입니다. 비밀번호가 1회용이라 노출되어도 재사용이 불가능하며, 비밀번호를 숨겨진 알고리즘을 이용하여 생성해내므로 서버와의 접속 없이도 생성이 가능하여 중간 과정에서 패킷이 유출되는 위험을 없앨 수 있습니다.

OTP는 크게 HMAC을 기반으로 하는 HOTP와 시간을 기반으로 하는 TOTP로 나뉘게 됩니다.

2 - 1 TOTP (Time Based - OTP)

스크린샷 2023-10-31 오후 3 37 18

TOTP는 시간을 기반으로 일회용 비밀번호를 생성합니다. 서버와 사용자의 생성기는 서로 교류하지 않지만, 시간을 seed로 하여 같은 알고리즘으로 생성 매칭을 하기 때문에 서로 시간이 동기화되는 것이 중요합니다. seed 시간은 정확한 시간을 맞추는 것이 아닌, 특정 time window 유닛으로 작동하므로 적정 수준의 허용범위가 존재합니다. 즉 서로 몇 초 차이가 나도 동작하게 됩니다.

2 - 2 HOTP (HMAC Based OTP)

스크린샷 2023-10-31 오후 3 37 48
HOTP는 카운터 값에 기반하여 OTP를 생성합니다. 사용자와 서버 모두가 같은 카운터 값을 유지하며, 매번 인증이 성공할 때마다 카운터는 증가합니다. HMAC (Keyed-Hash Message Authentication Code)을 사용하여 이 카운터 값에 따라 다른 일회용 비밀번호를 생성합니다.

3. 적용 방법

public class HOTP {

    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";

    public static int generateOTP(byte[] secret, long counter, int codeDigits) throws Exception {
        // 카운터를 바이트 단위로 변환
        byte[] data = ByteBuffer.allocate(8).putLong(counter).array();

        // Step 1: secret key와 카운터를 사용하여 HMAC-SHA256 해시 생성
        Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
        mac.init(new SecretKeySpec(secret, ""));
        byte[] hash = mac.doFinal(data);

        // Step 2: hash로 OTP 생성
        int offset = hash[hash.length - 1] & 0xF;
        int binary = ((hash[offset] & 0x7F) << 24)
                | ((hash[offset + 1] & 0xFF) << 16)
                | ((hash[offset + 2] & 0xFF) << 8)
                | (hash[offset + 3] & 0xFF);

        // Step 3: OTP 값 생성
        int otp = binary % (int) Math.pow(10, codeDigits);

        return otp;
    }

    public static void main(String[] args) throws Exception {
        // 예시
        byte[] secret = "SECRET_KEY".getBytes(); // 실제로는 예상하지 못하는 비밀 키를 사용해야 합니다. 
        long counter = 0; // counter 값은 안전하게 저장되어야 합니다. 
        int codeDigits = 4; // OTP 값 비밀번호 자리수

        int otp = generateOTP(secret, counter, codeDigits);
        System.out.println("Generated OTP: " + otp);
    }
}

3 - 1 코드 설명

  1. HMAC-SHA256 해시를 생성합니다.
// 카운터를 바이트 단위로 변환
byte[] data = ByteBuffer.allocate(8).putLong(counter).array();

// Step 1: secret key와 카운터를 사용하여 HMAC-SHA256 해시 생성
Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
mac.init(new SecretKeySpec(secret, ""));
byte[] hash = mac.doFinal(data);
  1. Hash로 OTP를 생성합니다.
// Step 2: hash로 OTP 생성
int offset = hash[hash.length - 1] & 0xF;
  • 여기서 hash[hash.length - 1]는 HMAC 해시의 마지막 바이트 값을 가져옵니다.
  • & 0xF는 비트 AND 연산을 통해 마지막 바이트의 하위 4비트를 추출합니다. 결과는 0에서 15 사이의 값입니다. 이 값은 해시의 어떤 부분을 OTP 값으로 사용할지를 결정하는 offset을 결정합니다.
int binary = ((hash[offset] & 0x7F) << 24)
            | ((hash[offset + 1] & 0xFF) << 16)
            | ((hash[offset + 2] & 0xFF) << 8)
            | (hash[offset + 3] & 0xFF);
  • 이 부분에서는 위에서 결정된 offset부터 시작하여 4개의 연속된 바이트를 추출하여 32비트 정수 값을 생성합니다.
  • & 0x7F는 첫 바이트의 최상위 비트를 0으로 설정하여 결과 값이 양수가 되도록 보장합니다.
  • 각 바이트 값은 적절한 위치로 이동 (<< 24, << 16, << 8) 또는 그대로 둔 후, 또는(|) 연산을 사용하여 결합됩니다. 이렇게 함으로써 32비트의 binary 값을 얻습니다.

3 - 3 다른 OTP 구현 방법

위의 코드는 직접 구현한 HOTP이지만 표준 스펙을 따른 여러 라이브러리가 존재합니다.

https://github.com/BastiaanJansen/otp-java

위의 코드를 예시로 들어보겠습니다.

먼저 의존성을 추가해줍니다.

implementation 'com.github.bastiaanjansen:otp-java:2.0.3'

사용 방법은 간단합니다. HOTP 생성기를 만들고, 비밀번호를 생성해주기만 하면 됩니다.

long counter = 0L;

HOTPGenerator hotp = new HOTPGenerator.Builder(secret)
    .withPasswordLength(6)
  .withAlgorithm(HMACAlgorithm.SHA512)
  .build();

String generate = hotp.generate(counter);

사진 출처 : https://www.onelogin.com/learn/otp-totp-hotp

'업브렐라' 카테고리의 다른 글

CloudFront 403 / 404 에러 해결  (1) 2023.12.23
Upbrella 버그 수정기  (0) 2023.12.19
협업지점 조회 성능 개선, N+1 해결  (0) 2023.10.15
캐시를 통한 성능 최적화  (0) 2023.09.25
Loki를 통한 로그 모니터링  (0) 2023.09.24