Java
[Java] Google OTP 개발기 - 구글OTP 쉬워요!
닥치고개발
2023. 7. 6. 13:01
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"));
}
}
728x90