Check whether system clock is reasonable at startup.

This commit is contained in:
akwizgran
2021-06-23 16:32:32 +01:00
committed by Torsten Grote
parent 80749fec09
commit 802f64e309
5 changed files with 75 additions and 10 deletions

View File

@@ -22,6 +22,7 @@ public interface LifecycleManager {
*/ */
enum StartResult { enum StartResult {
ALREADY_RUNNING, ALREADY_RUNNING,
CLOCK_ERROR,
DB_ERROR, DB_ERROR,
DATA_TOO_OLD_ERROR, DATA_TOO_OLD_ERROR,
DATA_TOO_NEW_ERROR, DATA_TOO_NEW_ERROR,
@@ -29,6 +30,26 @@ public interface LifecycleManager {
SUCCESS 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.
* <p/>
* 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.
* <p/>
* 1 Jan 2121, 00:00:00 UTC
*/
long MAX_REASONABLE_TIME_MS = 4_765_132_800_000L;
/** /**
* The state the lifecycle can be in. * The state the lifecycle can be in.
* Returned by {@link #getLifecycleState()} * Returned by {@link #getLifecycleState()}

View File

@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.lifecycle.Service;
import org.briarproject.bramble.api.lifecycle.ServiceException; import org.briarproject.bramble.api.lifecycle.ServiceException;
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; 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.STARTING_SERVICES;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.STOPPING; 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.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_NEW_ERROR;
import static org.briarproject.bramble.api.lifecycle.LifecycleManager.StartResult.DATA_TOO_OLD_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; 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 DatabaseComponent db;
private final EventBus eventBus; private final EventBus eventBus;
private final Clock clock;
private final List<Service> services; private final List<Service> services;
private final List<OpenDatabaseHook> openDatabaseHooks; private final List<OpenDatabaseHook> openDatabaseHooks;
private final List<ExecutorService> executors; private final List<ExecutorService> executors;
@@ -63,9 +66,11 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
private volatile LifecycleState state = STARTING; private volatile LifecycleState state = STARTING;
@Inject @Inject
LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus) { LifecycleManagerImpl(DatabaseComponent db, EventBus eventBus,
Clock clock) {
this.db = db; this.db = db;
this.eventBus = eventBus; this.eventBus = eventBus;
this.clock = clock;
services = new CopyOnWriteArrayList<>(); services = new CopyOnWriteArrayList<>();
openDatabaseHooks = new CopyOnWriteArrayList<>(); openDatabaseHooks = new CopyOnWriteArrayList<>();
executors = new CopyOnWriteArrayList<>(); executors = new CopyOnWriteArrayList<>();
@@ -99,6 +104,13 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
LOG.info("Already starting or stopping"); LOG.info("Already starting or stopping");
return ALREADY_RUNNING; 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 { try {
LOG.info("Opening database"); LOG.info("Opening database");
long start = now(); long start = now();

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook; import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook;
import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent; 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.BrambleMockTestCase;
import org.briarproject.bramble.test.DbExpectations; import org.briarproject.bramble.test.DbExpectations;
import org.junit.Before; import org.junit.Before;
@@ -14,6 +15,9 @@ import org.junit.Test;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import static junit.framework.TestCase.assertTrue; 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.api.lifecycle.LifecycleManager.StartResult.SUCCESS;
import static org.briarproject.bramble.test.TestUtils.getSecretKey; import static org.briarproject.bramble.test.TestUtils.getSecretKey;
import static org.junit.Assert.assertEquals; 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 DatabaseComponent db = context.mock(DatabaseComponent.class);
private final EventBus eventBus = context.mock(EventBus.class); private final EventBus eventBus = context.mock(EventBus.class);
private final Clock clock = context.mock(Clock.class);
private final SecretKey dbKey = getSecretKey(); private final SecretKey dbKey = getSecretKey();
@@ -29,16 +34,19 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase {
@Before @Before
public void setUp() { public void setUp() {
lifecycleManager = new LifecycleManagerImpl(db, eventBus); lifecycleManager = new LifecycleManagerImpl(db, eventBus, clock);
} }
@Test @Test
public void testOpenDatabaseHooksAreCalledAtStartup() throws Exception { public void testOpenDatabaseHooksAreCalledAtStartup() throws Exception {
long now = System.currentTimeMillis();
Transaction txn = new Transaction(null, false); Transaction txn = new Transaction(null, false);
AtomicBoolean called = new AtomicBoolean(false); AtomicBoolean called = new AtomicBoolean(false);
OpenDatabaseHook hook = transaction -> called.set(true); OpenDatabaseHook hook = transaction -> called.set(true);
context.checking(new DbExpectations() {{ context.checking(new DbExpectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(db).open(dbKey, lifecycleManager); oneOf(db).open(dbKey, lifecycleManager);
will(returnValue(false)); will(returnValue(false));
oneOf(db).transaction(with(false), withDbRunnable(txn)); oneOf(db).transaction(with(false), withDbRunnable(txn));
@@ -51,4 +59,26 @@ public class LifecycleManagerImplTest extends BrambleMockTestCase {
assertEquals(SUCCESS, lifecycleManager.startServices(dbKey)); assertEquals(SUCCESS, lifecycleManager.startServices(dbKey));
assertTrue(called.get()); 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));
}
} }

View File

@@ -51,26 +51,27 @@ public class StartupFailureActivity extends BaseActivity implements
} }
// show proper error message // show proper error message
String errorMsg; int errorRes;
switch (result) { switch (result) {
case CLOCK_ERROR:
errorRes = R.string.startup_failed_clock_error;
break;
case DATA_TOO_OLD_ERROR: case DATA_TOO_OLD_ERROR:
errorMsg = errorRes = R.string.startup_failed_data_too_old_error;
getString(R.string.startup_failed_data_too_old_error);
break; break;
case DATA_TOO_NEW_ERROR: case DATA_TOO_NEW_ERROR:
errorMsg = errorRes = R.string.startup_failed_data_too_new_error;
getString(R.string.startup_failed_data_too_new_error);
break; break;
case DB_ERROR: case DB_ERROR:
errorMsg = getString(R.string.startup_failed_db_error); errorRes = R.string.startup_failed_db_error;
break; break;
case SERVICE_ERROR: case SERVICE_ERROR:
errorMsg = getString(R.string.startup_failed_service_error); errorRes = R.string.startup_failed_service_error;
break; break;
default: default:
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
showInitialFragment(ErrorFragment.newInstance(errorMsg)); showInitialFragment(ErrorFragment.newInstance(getString(errorRes)));
} }
@Override @Override

View File

@@ -49,6 +49,7 @@
<string name="startup_failed_notification_title">Briar could not start</string> <string name="startup_failed_notification_title">Briar could not start</string>
<string name="startup_failed_notification_text">Tap for more information.</string> <string name="startup_failed_notification_text">Tap for more information.</string>
<string name="startup_failed_activity_title">Briar Startup Failure</string> <string name="startup_failed_activity_title">Briar Startup Failure</string>
<string name="startup_failed_clock_error">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.</string>
<string name="startup_failed_db_error">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.</string> <string name="startup_failed_db_error">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.</string>
<string name="startup_failed_data_too_old_error">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.</string> <string name="startup_failed_data_too_old_error">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.</string>
<string name="startup_failed_data_too_new_error">This version of the app is too old. Please upgrade to the latest version and try again.</string> <string name="startup_failed_data_too_new_error">This version of the app is too old. Please upgrade to the latest version and try again.</string>