728x90

구글 OTP 백엔드 관리 시스템에 2차 인증이 필요해서 알아보게 되었다.

생각보다 어렵지 않았다.

리서치 조금 해보니 답이 나와있었다.

제일 신기한건 서버를 타는것이 아니였다.

  • 알고리즘을 통한 검증인거다
  • 은행에서 사용하는 OTP도 결국 이와같은 방식일 것 이라 생각된다.

그럼 지금부터 코드를 살펴보자 java로 작성했다

@Service
@RequiredArgsConstructor
public class OtpService {

    private final UserMapper userMapper;
    private final SecurityService securityService;

    private boolean validOtp(String code, String encodedKey) {
        try {
            // 키, 코드, 시간으로 일회용 비밀번호가 맞는지 일치 여부 확인.
            return checkOtpCode(encodedKey, Integer.parseInt(code), new Date().getTime() / 30000);
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        return false;
    }

    public String addOtpKey(String email) {
        User user = userMapper.getUserByLoginId(email.replaceAll("[^A-Za-z0-9]", ""));
        if (user != null) {
            user.setLogin(securityService.encryptAES256(generateOtpKey()));
            userMapper.insertOtp(user);
            return securityService.decryptAES256(userMapper.getUserOtp(user.getId()));
        }
        return null;
    }

    public boolean isValidOtp(String email, String code) {
        try {
            User user = userMapper.getUserByLoginId(email.replaceAll("[^A-Za-z0-9]", ""));
            if (user != null) {
                final String encodedKey = securityService.decryptAES256(userMapper.getUserOtp(user.getId()));
                return validOtp(code, encodedKey);
            }
        } catch (Exception e) {
        }
        return false;
    }

    private String generateOtpKey() {
        final int MAX_LENGTH = 10;
        // Allocating the buffer
        //      byte[] buffer = new byte[secretSize + numOfScratchCodes * scratchCodeSize];
        byte[] buffer = new byte[5 + 5 * 5];

        // Filling the buffer with random numbers.
        // Notice: you want to reuse the same random generator
        // while generating larger random number sequences.
        new Random().nextBytes(buffer);

        // Getting the key and converting it to Base32
        Base32 codec = new Base32();
        //      byte[] secretKey = Arrays.copyOf(buffer, secretSize);
        byte[] secretKey = Arrays.copyOf(buffer, 10);
        byte[] bEncodedKey = codec.encode(secretKey);

        // 생성된 Key!
        String encodedKey = new String(bEncodedKey);

        System.out.println("encodedKey : " + encodedKey);

        // String url = getQRBarcodeURL(email, encodedKey); // 생성된 바코드 주소!

        return encodedKey;
    }

    private String getQRBarcodeURL(String email, String secret) {
        String format = "https://www.google.com/chart?chs=300x300&chld=M%%7C0&cht=qr&chl=otpauth://totp/%s%%3Fsecret%%3D%s";
        return String.format(format, email, secret);
    }

    private boolean checkOtpCode(String secret, long code, long t) throws NoSuchAlgorithmException, InvalidKeyException {
        Base32 codec = new Base32();
        byte[] decodedKey = codec.decode(secret);

        // Window is used to check codes generated in the near past.
        // You can use this value to tune how far you're willing to go.
        int window = 3;
        for (int i = -window; i <= window; ++i) {
            long hash = verifyCode(decodedKey, t + i);

            if (hash == code) {
                return true;
            }
        }

        // The validation code is invalid.
        return false;
    }

    private int verifyCode(byte[] key, long t)
            throws NoSuchAlgorithmException, InvalidKeyException {
        byte[] data = new byte[8];
        long value = t;
        for (int i = 8; i-- > 0; value >>>= 8) {
            data[i] = (byte) value;
        }

        SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(signKey);
        byte[] hash = mac.doFinal(data);

        int offset = hash[20 - 1] & 0xF;

        // We're using a long because Java hasn't got unsigned int.
        long truncatedHash = 0;
        for (int i = 0; i < 4; ++i) {
            truncatedHash <<= 8;
            // We are dealing with signed bytes:
            // we just keep the first byte.
            truncatedHash |= (hash[offset + i] & 0xFF);
        }

        truncatedHash &= 0x7FFFFFFF;
        truncatedHash %= 1000000;

        return (int) truncatedHash;
    }
}

간단한 사용법은 아래와 같이 쓰면 된다

  • isValidOtp 함수를 통해서 검증할 코드만 넘기면 된다
  • OPT 키 생성은 addOtpKey 메소드를 통해서 생성할 수 있다.
@SpringBootTest
class OtpServiceTest {

    @Autowired
    private OtpService otpService;

    @Test
    void 키_유효성_검사() {
        String code = "628212";
        final boolean validOtp = otpService.isValidOtp("dev@shutupdev.com", code);
        Assertions.assertEquals(validOtp, false);
    }

    @Test
    void 키생성_테스트() {
        assertNotNull(otpService.addOtpKey("dev@shutupdev.com"));
    }
}
복사했습니다!