diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java index ab14b669f..dcf8e8ce9 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/lifecycle/LifecycleManager.java @@ -22,6 +22,7 @@ public interface LifecycleManager { */ enum StartResult { ALREADY_RUNNING, + CLOCK_ERROR, DB_ERROR, DATA_TOO_OLD_ERROR, DATA_TOO_NEW_ERROR, @@ -29,6 +30,26 @@ public interface LifecycleManager { SUCCESS } + /** + * The minimum reasonable value for the system clock, in milliseconds + * since the Unix epoch. {@link #startServices(SecretKey)} will return + * {@link StartResult#CLOCK_ERROR} if the system clock reports an earlier + * time. + *

+ * 1 Jan 2021, 00:00:00 UTC + */ + long MIN_REASONABLE_TIME_MS = 1_609_459_200_000L; + + /** + * The maximum reasonable value for the system clock, in milliseconds + * since the Unix epoch. {@link #startServices(SecretKey)} will return + * {@link StartResult#CLOCK_ERROR} if the system clock reports a later + * time. + *

+ * 1 Jan 2121, 00:00:00 UTC + */ + long MAX_REASONABLE_TIME_MS = 4_765_132_800_000L; + /** * The state the lifecycle can be in. * Returned by {@link #getLifecycleState()} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java index fc1259d2a..5efab84ca 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/lifecycle/LifecycleManagerImpl.java @@ -12,6 +12,7 @@ import org.briarproject.bramble.api.lifecycle.Service; import org.briarproject.bramble.api.lifecycle.ServiceException; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.system.Clock; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; @@ -34,6 +35,7 @@ import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleS import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING; +import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.CLOCK_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_NEW_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_OLD_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DB_ERROR; @@ -52,6 +54,7 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener { private final DatabaseComponent db; private final EventBus eventBus; + private final Clock clock; private final List services; private final List openDatabaseHooks; private final List executors; @@ -63,9 +66,11 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener { private volatile LifecycleState state = STARTING; @Inject - LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus) { + LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus, + Clock clock) { this.db = db; this.eventBus = eventBus; + this.clock = clock; services = new CopyOnWriteArrayList<>(); openDatabaseHooks = new CopyOnWriteArrayList<>(); executors = new CopyOnWriteArrayList<>(); @@ -99,6 +104,13 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener { LOG.info("Already starting or stopping"); return ALREADY_RUNNING; } + long now = clock.currentTimeMillis(); + if (now < MIN_REASONABLE_TIME_MS || now > MAX_REASONABLE_TIME_MS) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("System clock is unreasonable: " + now); + } + return CLOCK_ERROR; + } try { LOG.info("Opening database"); long start = now(); diff --git a/bramble-core/src/test/java/org/briarproject/bramble/lifecycle/LifecycleManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/lifecycle/LifecycleManagerImplTest.java index 09ce697bf..079a02b5e 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/lifecycle/LifecycleManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/lifecycle/LifecycleManagerImplTest.java @@ -6,6 +6,7 @@ import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent; +import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.DbExpectations; import org.junit.Before; @@ -14,6 +15,9 @@ import org.junit.Test; import java.util.concurrent.atomic.AtomicBoolean; import static junit.framework.TestCase.assertTrue; +import static org.briarproject.bramble.api.lifecycle.LifecycleManager.MAX_REASONABLE_TIME_MS; +import static org.briarproject.bramble.api.lifecycle.LifecycleManager.MIN_REASONABLE_TIME_MS; +import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.CLOCK_ERROR; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.SUCCESS; import static org.briarproject.bramble.test.TestUtils.getSecretKey; import static org.junit.Assert.assertEquals; @@ -22,6 +26,7 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase { private final DatabaseComponent db = context.mock(DatabaseComponent.class); private final EventBus eventBus = context.mock(EventBus.class); + private final Clock clock = context.mock(Clock.class); private final SecretKey dbKey = getSecretKey(); @@ -29,16 +34,19 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase { @Before public void setUp() { - lifecycleManager = new LifecycleManagerImpl(db, eventBus); + lifecycleManager = new LifecycleManagerImpl(db, eventBus, clock); } @Test public void testOpenDatabaseHooksAreCalledAtStartup() throws Exception { + long now = System.currentTimeMillis(); Transaction txn = new Transaction(null, false); AtomicBoolean called = new AtomicBoolean(false); OpenDatabaseHook hook = transaction -> called.set(true); context.checking(new DbExpectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); oneOf(db).open(dbKey, lifecycleManager); will(returnValue(false)); oneOf(db).transaction(with(false), withDbRunnable(txn)); @@ -51,4 +59,26 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase { assertEquals(SUCCESS, lifecycleManager.startServices(dbKey)); assertTrue(called.get()); } + + @Test + public void testStartupFailsIfClockIsUnreasonablyBehind() { + + context.checking(new DbExpectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(MIN_REASONABLE_TIME_MS - 1)); + }}); + + assertEquals(CLOCK_ERROR, lifecycleManager.startServices(dbKey)); + } + + @Test + public void testStartupFailsIfClockIsUnreasonablyAhead() { + + context.checking(new DbExpectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(MAX_REASONABLE_TIME_MS + 1)); + }}); + + assertEquals(CLOCK_ERROR, lifecycleManager.startServices(dbKey)); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/StartupFailureActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/StartupFailureActivity.java index d92a2b58d..5f1e29fa2 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/StartupFailureActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/StartupFailureActivity.java @@ -51,26 +51,27 @@ public class StartupFailureActivity extends BaseActivity implements } // show proper error message - String errorMsg; + int errorRes; switch (result) { + case CLOCK_ERROR: + errorRes = R.string.startup_failed_clock_error; + break; case DATA_TOO_OLD_ERROR: - errorMsg = - getString(R.string.startup_failed_data_too_old_error); + errorRes = R.string.startup_failed_data_too_old_error; break; case DATA_TOO_NEW_ERROR: - errorMsg = - getString(R.string.startup_failed_data_too_new_error); + errorRes = R.string.startup_failed_data_too_new_error; break; case DB_ERROR: - errorMsg = getString(R.string.startup_failed_db_error); + errorRes = R.string.startup_failed_db_error; break; case SERVICE_ERROR: - errorMsg = getString(R.string.startup_failed_service_error); + errorRes = R.string.startup_failed_service_error; break; default: throw new IllegalArgumentException(); } - showInitialFragment(ErrorFragment.newInstance(errorMsg)); + showInitialFragment(ErrorFragment.newInstance(getString(errorRes))); } @Override diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index ab121d58c..999a8cb3a 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -49,6 +49,7 @@ Briar could not start Tap for more information. Briar Startup Failure + Briar was unable to start because your device\'s clock is wrong. Please set your device\'s clock to the right time and try again. For some reason, your Briar database is corrupted beyond repair. Your account, your data and all your contacts are lost. Unfortunately, you need to reinstall Briar or set up a new account by choosing \'I have forgotten my password\' at the password prompt. Your account was created with an old version of this app and cannot be opened with this version. You must either reinstall the old version or set up a new account by choosing \'I have forgotten my password\' at the password prompt. This version of the app is too old. Please upgrade to the latest version and try again.