KEYCLOAK-18880 TimeBasedOTP should use look-around to mitigate clock skew

Previously the TimeBasedOTP only looked behind to mitigate clock skew.
We now look around (look ahead + look behind) to better accommodate clock skew.
This commit is contained in:
Thomas Darimont 2021-08-22 22:31:04 +02:00 committed by Marek Posolda
parent 1c2752300b
commit 5898f9c390

View file

@ -42,10 +42,10 @@ public class TimeBasedOTP extends HmacOTP {
* @param algorithm the encryption algorithm * @param algorithm the encryption algorithm
* @param numberDigits the number of digits for tokens * @param numberDigits the number of digits for tokens
* @param timeIntervalInSeconds the number of seconds a token is valid * @param timeIntervalInSeconds the number of seconds a token is valid
* @param lookAheadWindow the number of previous intervals that should be used to validate tokens. * @param lookAroundWindow the number of previous and following intervals that should be used to validate tokens.
*/ */
public TimeBasedOTP(String algorithm, int numberDigits, int timeIntervalInSeconds, int lookAheadWindow) { public TimeBasedOTP(String algorithm, int numberDigits, int timeIntervalInSeconds, int lookAroundWindow) {
super(numberDigits, algorithm, lookAheadWindow); super(numberDigits, algorithm, lookAroundWindow);
this.clock = new Clock(timeIntervalInSeconds); this.clock = new Clock(timeIntervalInSeconds);
} }
@ -60,8 +60,9 @@ public class TimeBasedOTP extends HmacOTP {
String steps = Long.toHexString(T).toUpperCase(); String steps = Long.toHexString(T).toUpperCase();
// Just get a 16 digit string // Just get a 16 digit string
while (steps.length() < 16) while (steps.length() < 16) {
steps = "0" + steps; steps = "0" + steps;
}
return generateOTP(secretKey, steps, this.numberDigits, this.algorithm); return generateOTP(secretKey, steps, this.numberDigits, this.algorithm);
} }
@ -71,17 +72,21 @@ public class TimeBasedOTP extends HmacOTP {
* *
* @param token OTP string to validate * @param token OTP string to validate
* @param secret Shared secret * @param secret Shared secret
* @return * @return true of the token is valid
*/ */
public boolean validateTOTP(String token, byte[] secret) { public boolean validateTOTP(String token, byte[] secret) {
long currentInterval = this.clock.getCurrentInterval(); long currentInterval = this.clock.getCurrentInterval();
for (int i = this.lookAheadWindow; i >= 0; --i) { for (int i = 0; i <= (lookAheadWindow * 2); i++) {
String steps = Long.toHexString(currentInterval - i).toUpperCase(); long delta = clockSkewIndexToDelta(i);
long adjustedInterval = currentInterval + delta;
String steps = Long.toHexString(adjustedInterval).toUpperCase();
// Just get a 16 digit string // Just get a 16 digit string
while (steps.length() < 16) while (steps.length() < 16) {
steps = "0" + steps; steps = "0" + steps;
}
String candidate = generateOTP(new String(secret), steps, this.numberDigits, this.algorithm); String candidate = generateOTP(new String(secret), steps, this.numberDigits, this.algorithm);
@ -93,6 +98,13 @@ public class TimeBasedOTP extends HmacOTP {
return false; return false;
} }
/**
* maps 0, 1, 2, 3, 4, 5, 6, 7, ... to 0, -1, 1, -2, 2, -3, 3, ...
*/
public static long clockSkewIndexToDelta(int idx) {
return (idx + 1) / 2 * (1 - (idx % 2) * 2);
}
public void setCalendar(Calendar calendar) { public void setCalendar(Calendar calendar) {
this.clock.setCalendar(calendar); this.clock.setCalendar(calendar);
} }