구글 OTP Spring 구현

bestKimEver·2024년 8월 9일
0
post-custom-banner

OTP란?

구글 OTP(Google Authenticatior)

위키피디아 문서

  • 보안 문제로 요즘은 잘 사용되지 않고 Authy, Aegis, BitWarden이 더 많이 쓰인다는 것 같다.
  • 안드로이드 앱의 오픈 소스 포크가 깃허브에서 공개되었으나 21년도 이후로 업데이트가 중단된 상황이다.

구현

  • 지정된 WINDOW_SIZE만큼 현재 시각 앞뒤로 코드를 추가 검증한다. 0일 경우 현재 시각만을 기준으로 검사한다.
  • 랜덤하게 키를 생성할 때 SECRET_KEY_LENGTH를 참조해 키의 길이를 결정한다. 20 이상으로 하는 것이 추천됨.
    • key 길이가 len(key) % 8 == 1 일 경우 (예: 17자, 33자) 코드가 비정상적으로 생성되는 문제가 있다. Google Authenticator에서는 해당 문제를 이후 수정한 것으로 보이나 오픈 소스로 공개된 코드와 이를 활용해 작성한 코드 및 라이브러리(warrenstrange GoogleAuth)에는 문제가 여전하다. 해당 길이를 피해서 키를 생성해야 한다.

변수 설정

    // 양의 정수. 검증 실패 시 현재 시각 전후로 해당 변수로 지정된 횟수만큼 추가 검증
    final int WINDOW_SIZE = 3;

    // 길이 20 이상, 8의 배수인 양수로 설정하는 것을 추천함. 길이가 8의 배수 + 1이면 인증 제대로 안되는 문제 있음
    final int SECRET_KEY_LENGTH = 32;

OTP 코드 계산

    public int calculateOtpCodeFromKey(String otpKey, long t) throws NoSuchAlgorithmException, InvalidKeyException, EncoderException {
        byte[] timeData = new byte[8];
        long value = t;
        for (int i = 8; i-- > 0; value >>>= 8){
            timeData[i] = (byte) value;
        }

        Base32 codec = new Base32();
        byte[] decodedKey = codec.decode(otpKey);
        SecretKeySpec signKey = new SecretKeySpec(decodedKey, "HmacSHA1");
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(signKey);
        byte[] hashedOtpKey = mac.doFinal(timeData);

        int offset = hashedOtpKey[20 - 1] & 0xF;
        long truncatedHash = 0;
        for (int i = 0; i < 4; ++i) {
            truncatedHash <<= 8;
            // we just keep the first byte
            truncatedHash |= (hashedOtpKey[offset + i] & 0xFF);
        }

        truncatedHash &= 0x7FFFFFFF;
        truncatedHash %= 1000000;
        return (int) truncatedHash;
    }

OTP 코드 검증

    public Boolean verifyOtpCode(int otpCode, String otpKey) throws NoSuchAlgorithmException, InvalidKeyException, EncoderException {
        long t = new Date().getTime() / 30000;
        // 현재 시간 기준으로 우선 검사
        if (otpCode == calculateOtpCodeFromKey(otpKey, t)){
            return true;
        }
        // 최초 검사 실패 시 현재 시간 전후로 WINDOW_SIZE 만큼 추가 검사
        for (int i = -1 * this.WINDOW_SIZE; i <= this.WINDOW_SIZE; i++){
            if (i == 0){
                continue;
            }
            if (otpCode == calculateOtpCodeFromKey(otpKey, t + i)){
                return true;
            }
        }
        // 검사 실패: 코드 불일치
        return false;
    }

OTP 키 생성

    public String generateRandomBase32Key(){
        // Allocating the buffer
        byte[] buffer = new byte[this.SECRET_KEY_LENGTH * 3];
        // Filling the buffer with random numbers
        new SecureRandom().nextBytes(buffer);
        // Getting the key & converting it to Base32
        Base32 codec = new Base32();
        byte[] bEncodedKey = codec.encode(buffer) ;
        return new String(bEncodedKey).substring(0, this.SECRET_KEY_LENGTH);
    }

테스트

        String generatedKey = otpUtil.generateRandomBase32Key();
        long t = new Date().getTime() / 30000;
        System.out.println("now: " + new Date());
        int calculatedCode = otpUtil.calculateOtpCodeFromKey(generatedKey, t);
        System.out.println("key: " + generatedKey);
        System.out.println("code: " + calculatedCode);
        assert otpUtil.verifyOtpCode(calculatedCode, generatedKey);

참고한 문서

Google Authenticator: Using It With Your Own Java Authentication Server

[Java] Google Authenticator(Google OTP)를 이용한 개발.

profile
이제 3년차 개발새발자. 제가 보려고 정리해놓는 글이기 때문에 다소 미흡한 내용이 많습니다.
post-custom-banner

0개의 댓글