shwstppr commented on issue #9515: URL: https://github.com/apache/cloudstack/issues/9515#issuecomment-2809022974
@borisstoyanov cc @harikrishna-patnala this is a little tricky issue wrt time sync. There is no direct way to ascertain that the failure is due to time sync. Even in this case, it is just a TOTP mismatch. I've created a PR to log an error about the mismatch, but nothing about time sync. We can add a code like the following which would add a time sync warning for a 1 min (30s either side) but I'm not sure if it is worth, ``` diff --git a/plugins/user-two-factor-authenticators/totp/src/main/java/org/apache/cloudstack/auth/TotpUserTwoFactorAuthenticator.java b/plugins/user-two-factor-authenticators/totp/src/main/java/org/apache/cloudstack/auth/TotpUserTwoFactorAuthenticator.java index b722bd1439..a1ce3e43e3 100644 --- a/plugins/user-two-factor-authenticators/totp/src/main/java/org/apache/cloudstack/auth/TotpUserTwoFactorAuthenticator.java +++ b/plugins/user-two-factor-authenticators/totp/src/main/java/org/apache/cloudstack/auth/TotpUserTwoFactorAuthenticator.java @@ -16,6 +16,8 @@ package org.apache.cloudstack.auth; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; import javax.inject.Inject; import com.cloud.exception.CloudTwoFactorAuthenticationException; @@ -49,11 +51,15 @@ public class TotpUserTwoFactorAuthenticator extends AdapterBase implements UserT @Override public void check2FA(String code, UserAccount userAccount) throws CloudTwoFactorAuthenticationException { - String expectedCode = get2FACode(get2FAKey(userAccount)); + String secretKey = get2FAKey(userAccount); + String expectedCode = get2FACode(secretKey); if (expectedCode.equals(code)) { logger.info("2FA matches user's input"); return; } + if (isLikelyTimeDrift(secretKey, code)) { + logger.warn("2FA mismatch — code is valid for a different time window (likely time drift)"); + } String msg = "two-factor authentication code provided is invalid"; logger.error(msg); throw new CloudTwoFactorAuthenticationException(msg); @@ -70,6 +76,63 @@ public class TotpUserTwoFactorAuthenticator extends AdapterBase implements UserT return TOTP.getOTP(hexKey); } + public static String getOTP(String hexKey, long time) { + byte[] key; + try { + key = Hex.decodeHex(hexKey.toCharArray()); + } catch (Exception e) { + throw new RuntimeException("Invalid hex key", e); + } + + // Convert time to byte array (8-byte big-endian) + byte[] data = new byte[8]; + long value = time; + for (int i = 7; i >= 0; i--) { + data[i] = (byte) (value & 0xFF); + value >>= 8; + } + + try { + // HMAC-SHA1 + Mac mac = Mac.getInstance("HmacSHA1"); + SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1"); + mac.init(signKey); + byte[] hmac = mac.doFinal(data); + + // Dynamic Truncation + int offset = hmac[hmac.length - 1] & 0xF; + int binary = + ((hmac[offset] & 0x7F) << 24) | + ((hmac[offset + 1] & 0xFF) << 16) | + ((hmac[offset + 2] & 0xFF) << 8) | + (hmac[offset + 3] & 0xFF); + + int otp = binary % 1000000; // 6-digit code + return String.format("%06d", otp); + } catch (Exception e) { + throw new RuntimeException("Error generating TOTP", e); + } + } + + + private boolean isLikelyTimeDrift(String base32Secret, String inputCode) { + final int window = 1; + Base32 base32 = new Base32(); + byte[] keyBytes = base32.decode(base32Secret); + String hexKey = Hex.encodeHexString(keyBytes); + + long currentTimeIndex = System.currentTimeMillis() / 1000 / 30; + + for (int i = -window; i <= window; i++) { + if (i == 0) continue; // skip current window + String otp = getOTP(hexKey, currentTimeIndex + i); + if (otp.equals(inputCode)) { + return true; + } + } + return false; + } + @Override public String setup2FAKey(UserAccount userAccount) { if (StringUtils.isNotEmpty(userAccount.getKeyFor2fa())) { ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: commits-unsubscr...@cloudstack.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org