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

Reply via email to