From eede4e50ca51ab9aef557d9d2381db3605f888b9 Mon Sep 17 00:00:00 2001 From: Ehren Kret Date: Tue, 26 May 2020 13:38:52 -0700 Subject: [PATCH] Use hashed UUID to spread last seen updates over a full day (#40) --- .../auth/BaseAccountAuthenticator.java | 24 ++++- .../textsecuregcm/util/Util.java | 14 ++- .../auth/BaseAccountAuthenticatorTest.java | 100 ++++++++++++++++++ 3 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/BaseAccountAuthenticatorTest.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java index ac494337b..197baf4cb 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/BaseAccountAuthenticator.java @@ -3,6 +3,8 @@ package org.whispersystems.textsecuregcm.auth; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.SharedMetricRegistries; +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.auth.basic.BasicCredentials; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.storage.Account; @@ -11,11 +13,12 @@ import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Util; +import java.time.Clock; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Optional; -import java.util.UUID; import static com.codahale.metrics.MetricRegistry.name; -import io.dropwizard.auth.basic.BasicCredentials; public class BaseAccountAuthenticator { @@ -31,9 +34,16 @@ public class BaseAccountAuthenticator { private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class); private final AccountsManager accountsManager; + private final Clock clock; public BaseAccountAuthenticator(AccountsManager accountsManager) { + this(accountsManager, Clock.systemUTC()); + } + + @VisibleForTesting + public BaseAccountAuthenticator(AccountsManager accountsManager, Clock clock) { this.accountsManager = accountsManager; + this.clock = clock; } public Optional authenticate(BasicCredentials basicCredentials, boolean enabledRequired) { @@ -80,9 +90,13 @@ public class BaseAccountAuthenticator { } } - private void updateLastSeen(Account account, Device device) { - if (device.getLastSeen() != Util.todayInMillis()) { - device.setLastSeen(Util.todayInMillis()); + @VisibleForTesting + public void updateLastSeen(Account account, Device device) { + final long lastSeenOffsetSeconds = Math.abs(account.getUuid().getLeastSignificantBits()) % ChronoUnit.DAYS.getDuration().toSeconds(); + final long todayInMillisWithOffset = Util.todayInMillisGivenOffsetFromNow(clock, Duration.ofSeconds(lastSeenOffsetSeconds).negated()); + + if (device.getLastSeen() < todayInMillisWithOffset) { + device.setLastSeen(Util.todayInMillis(clock)); accountsManager.update(account); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java index 71939f2f1..ad57cad5f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java @@ -23,6 +23,9 @@ import java.net.URLEncoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.time.Clock; +import java.time.Duration; +import java.time.temporal.ChronoField; import java.util.Arrays; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -186,6 +189,15 @@ public class Util { } public static long todayInMillis() { - return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis())); + return todayInMillis(Clock.systemUTC()); + } + + public static long todayInMillis(Clock clock) { + return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(clock.instant().toEpochMilli())); + } + + public static long todayInMillisGivenOffsetFromNow(Clock clock, Duration offset) { + final long currentTimeSeconds = offset.addTo(clock.instant()).getLong(ChronoField.INSTANT_SECONDS); + return TimeUnit.DAYS.toMillis(TimeUnit.SECONDS.toDays(currentTimeSeconds)); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/BaseAccountAuthenticatorTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/BaseAccountAuthenticatorTest.java new file mode 100644 index 000000000..dc9fe39a2 --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/auth/BaseAccountAuthenticatorTest.java @@ -0,0 +1,100 @@ +package org.whispersystems.textsecuregcm.tests.auth; + +import org.junit.Before; +import org.junit.Test; +import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; + +import java.time.Clock; +import java.time.Instant; +import java.util.Random; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class BaseAccountAuthenticatorTest { + + private final Random random = new Random(867_5309L); + private final long today = 1590451200000L; + private final long yesterday = today - 86_400_000L; + private final long oldTime = yesterday - 86_400_000L; + private final long currentTime = today + 68_000_000L; + + private AccountsManager accountsManager; + private BaseAccountAuthenticator baseAccountAuthenticator; + private Clock clock; + private Account acct1; + private Account acct2; + private Account oldAccount; + + @Before + public void setup() { + accountsManager = mock(AccountsManager.class); + clock = mock(Clock.class); + baseAccountAuthenticator = new BaseAccountAuthenticator(accountsManager, clock); + + acct1 = new Account("+14088675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null, null, null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null); + acct2 = new Account("+14098675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null, null, null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null); + oldAccount = new Account("+14108675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null, null, null, null, null, false, 0, null, oldTime, 0, null, 0, null)), null); + } + + @Test + public void testUpdateLastSeenMiddleOfDay() { + when(clock.instant()).thenReturn(Instant.ofEpochMilli(currentTime)); + + baseAccountAuthenticator.updateLastSeen(acct1, acct1.getDevices().stream().findFirst().get()); + baseAccountAuthenticator.updateLastSeen(acct2, acct2.getDevices().stream().findFirst().get()); + + verify(accountsManager, never()).update(acct1); + verify(accountsManager).update(acct2); + + assertThat(acct1.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(yesterday); + assertThat(acct2.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(today); + } + + @Test + public void testUpdateLastSeenStartOfDay() { + when(clock.instant()).thenReturn(Instant.ofEpochMilli(today)); + + baseAccountAuthenticator.updateLastSeen(acct1, acct1.getDevices().stream().findFirst().get()); + baseAccountAuthenticator.updateLastSeen(acct2, acct2.getDevices().stream().findFirst().get()); + + verify(accountsManager, never()).update(acct1); + verify(accountsManager, never()).update(acct2); + + assertThat(acct1.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(yesterday); + assertThat(acct2.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(yesterday); + } + + @Test + public void testUpdateLastSeenEndOfDay() { + when(clock.instant()).thenReturn(Instant.ofEpochMilli(today + 86_400_000L - 1)); + + baseAccountAuthenticator.updateLastSeen(acct1, acct1.getDevices().stream().findFirst().get()); + baseAccountAuthenticator.updateLastSeen(acct2, acct2.getDevices().stream().findFirst().get()); + + verify(accountsManager).update(acct1); + verify(accountsManager).update(acct2); + + assertThat(acct1.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(today); + assertThat(acct2.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(today); + } + + @Test + public void testNeverWriteYesterday() { + when(clock.instant()).thenReturn(Instant.ofEpochMilli(today)); + + baseAccountAuthenticator.updateLastSeen(oldAccount, oldAccount.getDevices().stream().findFirst().get()); + + verify(accountsManager).update(oldAccount); + + assertThat(oldAccount.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(today); + } +}