Merge "Make NtpTrustedTime safer / expand docs"

This commit is contained in:
Neil Fuller
2020-01-13 20:19:11 +00:00
committed by Gerrit Code Review
6 changed files with 259 additions and 138 deletions

View File

@@ -16,6 +16,8 @@
package android.util; package android.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage; import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
@@ -25,172 +27,270 @@ import android.net.Network;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.net.SntpClient; import android.net.SntpClient;
import android.os.SystemClock; import android.os.SystemClock;
import android.os.TimestampedValue;
import android.provider.Settings; import android.provider.Settings;
import android.text.TextUtils; import android.text.TextUtils;
import com.android.internal.annotations.GuardedBy;
import java.util.Objects;
import java.util.function.Supplier;
/** /**
* {@link TrustedTime} that connects with a remote NTP server as its trusted * A singleton that connects with a remote NTP server as its trusted time source. This class
* time source. * is thread-safe. The {@link #forceRefresh()} method is synchronous, i.e. it may occupy the
* current thread while performing an NTP request. All other threads calling {@link #forceRefresh()}
* will block during that request.
* *
* @hide * @hide
*/ */
public class NtpTrustedTime implements TrustedTime { public class NtpTrustedTime implements TrustedTime {
/**
* The result of a successful NTP query.
*
* @hide
*/
public static class TimeResult {
private final long mTimeMillis;
private final long mElapsedRealtimeMillis;
private final long mCertaintyMillis;
public TimeResult(long timeMillis, long elapsedRealtimeMillis, long certaintyMillis) {
mTimeMillis = timeMillis;
mElapsedRealtimeMillis = elapsedRealtimeMillis;
mCertaintyMillis = certaintyMillis;
}
public long getTimeMillis() {
return mTimeMillis;
}
public long getElapsedRealtimeMillis() {
return mElapsedRealtimeMillis;
}
public long getCertaintyMillis() {
return mCertaintyMillis;
}
/** Calculates and returns the current time accounting for the age of this result. */
public long currentTimeMillis() {
return mTimeMillis + getAgeMillis();
}
/** Calculates and returns the age of this result. */
public long getAgeMillis() {
return SystemClock.elapsedRealtime() - mElapsedRealtimeMillis;
}
@Override
public String toString() {
return "TimeResult{"
+ "mTimeMillis=" + mTimeMillis
+ ", mElapsedRealtimeMillis=" + mElapsedRealtimeMillis
+ ", mCertaintyMillis=" + mCertaintyMillis
+ '}';
}
}
private static final String TAG = "NtpTrustedTime"; private static final String TAG = "NtpTrustedTime";
private static final boolean LOGD = false; private static final boolean LOGD = false;
private static NtpTrustedTime sSingleton; private static NtpTrustedTime sSingleton;
private static Context sContext;
private final String mServer; @NonNull
private final long mTimeout; private final Context mContext;
private ConnectivityManager mCM; /**
* A supplier that returns the ConnectivityManager. The Supplier can return null if
* ConnectivityService isn't running yet.
*/
private final Supplier<ConnectivityManager> mConnectivityManagerSupplier =
new Supplier<ConnectivityManager>() {
private ConnectivityManager mConnectivityManager;
private boolean mHasCache; @Nullable
private long mCachedNtpTime; @Override
private long mCachedNtpElapsedRealtime; public synchronized ConnectivityManager get() {
private long mCachedNtpCertainty; // We can't do this at initialization time: ConnectivityService might not be running
// yet.
if (mConnectivityManager == null) {
mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
}
return mConnectivityManager;
}
};
private NtpTrustedTime(String server, long timeout) { // Declared volatile and accessed outside of synchronized blocks to avoid blocking reads during
if (LOGD) Log.d(TAG, "creating NtpTrustedTime using " + server); // forceRefresh().
mServer = server; private volatile TimeResult mTimeResult;
mTimeout = timeout;
private NtpTrustedTime(Context context) {
mContext = Objects.requireNonNull(context);
} }
@UnsupportedAppUsage @UnsupportedAppUsage
public static synchronized NtpTrustedTime getInstance(Context context) { public static synchronized NtpTrustedTime getInstance(Context context) {
if (sSingleton == null) { if (sSingleton == null) {
final Resources res = context.getResources(); Context appContext = context.getApplicationContext();
final ContentResolver resolver = context.getContentResolver(); sSingleton = new NtpTrustedTime(appContext);
final String defaultServer = res.getString(
com.android.internal.R.string.config_ntpServer);
final long defaultTimeout = res.getInteger(
com.android.internal.R.integer.config_ntpTimeout);
final String secureServer = Settings.Global.getString(
resolver, Settings.Global.NTP_SERVER);
final long timeout = Settings.Global.getLong(
resolver, Settings.Global.NTP_TIMEOUT, defaultTimeout);
final String server = secureServer != null ? secureServer : defaultServer;
sSingleton = new NtpTrustedTime(server, timeout);
sContext = context;
} }
return sSingleton; return sSingleton;
} }
@Override
@UnsupportedAppUsage @UnsupportedAppUsage
public boolean forceRefresh() { public boolean forceRefresh() {
// We can't do this at initialization time: ConnectivityService might not be running yet.
synchronized (this) { synchronized (this) {
if (mCM == null) { NtpConnectionInfo connectionInfo = getNtpConnectionInfo();
mCM = sContext.getSystemService(ConnectivityManager.class); if (connectionInfo == null) {
// missing server config, so no trusted time available
if (LOGD) Log.d(TAG, "forceRefresh: invalid server config");
return false;
} }
}
final Network network = mCM == null ? null : mCM.getActiveNetwork(); ConnectivityManager connectivityManager = mConnectivityManagerSupplier.get();
return forceRefresh(network); if (connectivityManager == null) {
} if (LOGD) Log.d(TAG, "forceRefresh: no ConnectivityManager");
return false;
public boolean forceRefresh(Network network) { }
if (TextUtils.isEmpty(mServer)) { final Network network = connectivityManager.getActiveNetwork();
// missing server, so no trusted time available final NetworkInfo ni = connectivityManager.getNetworkInfo(network);
return false; if (ni == null || !ni.isConnected()) {
} if (LOGD) Log.d(TAG, "forceRefresh: no connectivity");
return false;
// We can't do this at initialization time: ConnectivityService might not be running yet.
synchronized (this) {
if (mCM == null) {
mCM = sContext.getSystemService(ConnectivityManager.class);
} }
}
final NetworkInfo ni = mCM == null ? null : mCM.getNetworkInfo(network); if (LOGD) Log.d(TAG, "forceRefresh() from cache miss");
if (ni == null || !ni.isConnected()) { final SntpClient client = new SntpClient();
if (LOGD) Log.d(TAG, "forceRefresh: no connectivity"); final String serverName = connectionInfo.getServer();
return false; final int timeoutMillis = connectionInfo.getTimeoutMillis();
} if (client.requestTime(serverName, timeoutMillis, network)) {
long ntpCertainty = client.getRoundTripTime() / 2;
mTimeResult = new TimeResult(
if (LOGD) Log.d(TAG, "forceRefresh() from cache miss"); client.getNtpTime(), client.getNtpTimeReference(), ntpCertainty);
final SntpClient client = new SntpClient(); return true;
if (client.requestTime(mServer, (int) mTimeout, network)) { } else {
mHasCache = true; return false;
mCachedNtpTime = client.getNtpTime(); }
mCachedNtpElapsedRealtime = client.getNtpTimeReference();
mCachedNtpCertainty = client.getRoundTripTime() / 2;
return true;
} else {
return false;
} }
} }
@Override /**
* Only kept for UnsupportedAppUsage.
*
* @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
*/
@Deprecated
@UnsupportedAppUsage @UnsupportedAppUsage
public boolean hasCache() { public boolean hasCache() {
return mHasCache; return mTimeResult != null;
} }
/**
* Only kept for UnsupportedAppUsage.
*
* @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
*/
@Deprecated
@Override @Override
public long getCacheAge() { public long getCacheAge() {
if (mHasCache) { TimeResult timeResult = mTimeResult;
return SystemClock.elapsedRealtime() - mCachedNtpElapsedRealtime; if (timeResult != null) {
return SystemClock.elapsedRealtime() - timeResult.getElapsedRealtimeMillis();
} else { } else {
return Long.MAX_VALUE; return Long.MAX_VALUE;
} }
} }
@Override /**
public long getCacheCertainty() { * Only kept for UnsupportedAppUsage.
if (mHasCache) { *
return mCachedNtpCertainty; * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
} else { */
return Long.MAX_VALUE; @Deprecated
}
}
@Override
@UnsupportedAppUsage @UnsupportedAppUsage
public long currentTimeMillis() { public long currentTimeMillis() {
if (!mHasCache) { TimeResult timeResult = mTimeResult;
if (timeResult == null) {
throw new IllegalStateException("Missing authoritative time source"); throw new IllegalStateException("Missing authoritative time source");
} }
if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit"); if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit");
// current time is age after the last ntp cache; callers who // current time is age after the last ntp cache; callers who
// want fresh values will hit makeAuthoritative() first. // want fresh values will hit forceRefresh() first.
return mCachedNtpTime + getCacheAge(); return timeResult.currentTimeMillis();
}
@UnsupportedAppUsage
public long getCachedNtpTime() {
if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit");
return mCachedNtpTime;
}
@UnsupportedAppUsage
public long getCachedNtpTimeReference() {
return mCachedNtpElapsedRealtime;
} }
/** /**
* Returns the combination of {@link #getCachedNtpTime()} and {@link * Only kept for UnsupportedAppUsage.
* #getCachedNtpTimeReference()} as a {@link TimestampedValue}. This method is useful when
* passing the time to another component that will adjust for elapsed time.
* *
* @throws IllegalStateException if there is no cached value * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
*/ */
public TimestampedValue<Long> getCachedNtpTimeSignal() { @Deprecated
if (!mHasCache) { @UnsupportedAppUsage
throw new IllegalStateException("Missing authoritative time source"); public long getCachedNtpTime() {
} if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit");
if (LOGD) Log.d(TAG, "getCachedNtpTimeSignal() cache hit"); TimeResult timeResult = mTimeResult;
return timeResult == null ? 0 : timeResult.getTimeMillis();
return new TimestampedValue<>(mCachedNtpElapsedRealtime, mCachedNtpTime);
} }
/**
* Only kept for UnsupportedAppUsage.
*
* @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
*/
@Deprecated
@UnsupportedAppUsage
public long getCachedNtpTimeReference() {
TimeResult timeResult = mTimeResult;
return timeResult == null ? 0 : timeResult.getElapsedRealtimeMillis();
}
/**
* Returns an object containing the latest NTP information available. Can return {@code null} if
* no information is available.
*/
@Nullable
public TimeResult getCachedTimeResult() {
return mTimeResult;
}
private static class NtpConnectionInfo {
@NonNull private final String mServer;
private final int mTimeoutMillis;
NtpConnectionInfo(@NonNull String server, int timeoutMillis) {
mServer = Objects.requireNonNull(server);
mTimeoutMillis = timeoutMillis;
}
@NonNull
public String getServer() {
return mServer;
}
int getTimeoutMillis() {
return mTimeoutMillis;
}
}
@GuardedBy("this")
private NtpConnectionInfo getNtpConnectionInfo() {
final ContentResolver resolver = mContext.getContentResolver();
final Resources res = mContext.getResources();
final String defaultServer = res.getString(
com.android.internal.R.string.config_ntpServer);
final int defaultTimeoutMillis = res.getInteger(
com.android.internal.R.integer.config_ntpTimeout);
final String secureServer = Settings.Global.getString(
resolver, Settings.Global.NTP_SERVER);
final int timeoutMillis = Settings.Global.getInt(
resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis);
final String server = secureServer != null ? secureServer : defaultServer;
return TextUtils.isEmpty(server) ? null : new NtpConnectionInfo(server, timeoutMillis);
}
} }

View File

@@ -20,42 +20,48 @@ import android.compat.annotation.UnsupportedAppUsage;
/** /**
* Interface that provides trusted time information, possibly coming from an NTP * Interface that provides trusted time information, possibly coming from an NTP
* server. Implementations may cache answers until {@link #forceRefresh()}. * server.
* *
* @hide * @hide
* @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
*/ */
public interface TrustedTime { public interface TrustedTime {
/** /**
* Force update with an external trusted time source, returning {@code true} * Force update with an external trusted time source, returning {@code true}
* when successful. * when successful.
*
* @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
*/ */
@Deprecated
@UnsupportedAppUsage @UnsupportedAppUsage
public boolean forceRefresh(); public boolean forceRefresh();
/** /**
* Check if this instance has cached a response from a trusted time source. * Check if this instance has cached a response from a trusted time source.
*
* @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
*/ */
@Deprecated
@UnsupportedAppUsage @UnsupportedAppUsage
public boolean hasCache(); boolean hasCache();
/** /**
* Return time since last trusted time source contact, or * Return time since last trusted time source contact, or
* {@link Long#MAX_VALUE} if never contacted. * {@link Long#MAX_VALUE} if never contacted.
*
* @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
*/ */
@Deprecated
@UnsupportedAppUsage @UnsupportedAppUsage
public long getCacheAge(); public long getCacheAge();
/**
* Return certainty of cached trusted time in milliseconds, or
* {@link Long#MAX_VALUE} if never contacted. Smaller values are more
* precise.
*/
public long getCacheCertainty();
/** /**
* Return current time similar to {@link System#currentTimeMillis()}, * Return current time similar to {@link System#currentTimeMillis()},
* possibly using a cached authoritative time source. * possibly using a cached authoritative time source.
*
* @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
*/ */
@Deprecated
@UnsupportedAppUsage @UnsupportedAppUsage
public long currentTimeMillis(); long currentTimeMillis();
} }

View File

@@ -2167,8 +2167,9 @@ class AlarmManagerService extends SystemService {
@Override @Override
public long currentNetworkTimeMillis() { public long currentNetworkTimeMillis() {
final NtpTrustedTime time = NtpTrustedTime.getInstance(getContext()); final NtpTrustedTime time = NtpTrustedTime.getInstance(getContext());
if (time.hasCache()) { NtpTrustedTime.TimeResult ntpResult = time.getCachedTimeResult();
return time.currentTimeMillis(); if (ntpResult != null) {
return ntpResult.currentTimeMillis();
} else { } else {
throw new ParcelableException(new DateTimeException("Missing NTP fix")); throw new ParcelableException(new DateTimeException("Missing NTP fix"));
} }

View File

@@ -154,17 +154,20 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
private void onPollNetworkTimeUnderWakeLock(int event) { private void onPollNetworkTimeUnderWakeLock(int event) {
// Force an NTP fix when outdated // Force an NTP fix when outdated
if (mTime.getCacheAge() >= mPollingIntervalMs) { NtpTrustedTime.TimeResult cachedNtpResult = mTime.getCachedTimeResult();
if (cachedNtpResult == null || cachedNtpResult.getAgeMillis() >= mPollingIntervalMs) {
if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh"); if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh");
mTime.forceRefresh(); mTime.forceRefresh();
cachedNtpResult = mTime.getCachedTimeResult();
} }
if (mTime.getCacheAge() < mPollingIntervalMs) { if (cachedNtpResult != null && cachedNtpResult.getAgeMillis() < mPollingIntervalMs) {
// Obtained fresh fix; schedule next normal update // Obtained fresh fix; schedule next normal update
resetAlarm(mPollingIntervalMs); resetAlarm(mPollingIntervalMs);
// Suggest the time to the time detector. It may choose use it to set the system clock. // Suggest the time to the time detector. It may choose use it to set the system clock.
TimestampedValue<Long> timeSignal = mTime.getCachedNtpTimeSignal(); TimestampedValue<Long> timeSignal = new TimestampedValue<>(
cachedNtpResult.getElapsedRealtimeMillis(), cachedNtpResult.getTimeMillis());
NetworkTimeSuggestion timeSuggestion = new NetworkTimeSuggestion(timeSignal); NetworkTimeSuggestion timeSuggestion = new NetworkTimeSuggestion(timeSignal);
timeSuggestion.addDebugInfo("Origin: NetworkTimeUpdateServiceImpl. event=" + event); timeSuggestion.addDebugInfo("Origin: NetworkTimeUpdateServiceImpl. event=" + event);
mTimeDetector.suggestNetworkTime(timeSuggestion); mTimeDetector.suggestNetworkTime(timeSuggestion);
@@ -275,8 +278,11 @@ public class NetworkTimeUpdateServiceImpl extends Binder implements NetworkTimeU
TimeUtils.formatDuration(mPollingIntervalShorterMs, pw); TimeUtils.formatDuration(mPollingIntervalShorterMs, pw);
pw.println("\nTryAgainTimesMax: " + mTryAgainTimesMax); pw.println("\nTryAgainTimesMax: " + mTryAgainTimesMax);
pw.println("\nTryAgainCounter: " + mTryAgainCounter); pw.println("\nTryAgainCounter: " + mTryAgainCounter);
pw.println("NTP cache age: " + mTime.getCacheAge()); NtpTrustedTime.TimeResult ntpResult = mTime.getCachedTimeResult();
pw.println("NTP cache certainty: " + mTime.getCacheCertainty()); pw.println("NTP cache result: " + ntpResult);
if (ntpResult != null) {
pw.println("NTP result age: " + ntpResult.getAgeMillis());
}
pw.println(); pw.println();
} }
} }

View File

@@ -130,7 +130,8 @@ class NtpTimeHelper {
// force refresh NTP cache when outdated // force refresh NTP cache when outdated
boolean refreshSuccess = true; boolean refreshSuccess = true;
if (mNtpTime.getCacheAge() >= NTP_INTERVAL) { NtpTrustedTime.TimeResult ntpResult = mNtpTime.getCachedTimeResult();
if (ntpResult == null || ntpResult.getAgeMillis() >= NTP_INTERVAL) {
// Blocking network operation. // Blocking network operation.
refreshSuccess = mNtpTime.forceRefresh(); refreshSuccess = mNtpTime.forceRefresh();
} }
@@ -140,17 +141,17 @@ class NtpTimeHelper {
// only update when NTP time is fresh // only update when NTP time is fresh
// If refreshSuccess is false, cacheAge does not drop down. // If refreshSuccess is false, cacheAge does not drop down.
if (mNtpTime.getCacheAge() < NTP_INTERVAL) { ntpResult = mNtpTime.getCachedTimeResult();
long time = mNtpTime.getCachedNtpTime(); if (ntpResult != null && ntpResult.getAgeMillis() < NTP_INTERVAL) {
long timeReference = mNtpTime.getCachedNtpTimeReference(); long time = ntpResult.getTimeMillis();
long certainty = mNtpTime.getCacheCertainty(); long timeReference = ntpResult.getElapsedRealtimeMillis();
long certainty = ntpResult.getCertaintyMillis();
if (DEBUG) { if (DEBUG) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
Log.d(TAG, "NTP server returned: " Log.d(TAG, "NTP server returned: "
+ time + " (" + new Date(time) + time + " (" + new Date(time) + ")"
+ ") reference: " + timeReference + " ntpResult: " + ntpResult
+ " certainty: " + certainty
+ " system time offset: " + (time - now)); + " system time offset: " + (time - now));
} }

View File

@@ -3,6 +3,7 @@ package com.android.server.location;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
@@ -52,8 +53,10 @@ public class NtpTimeHelperTest {
@Test @Test
public void handleInjectNtpTime_cachedAgeLow_injectTime() throws InterruptedException { public void handleInjectNtpTime_cachedAgeLow_injectTime() throws InterruptedException {
doReturn(NtpTimeHelper.NTP_INTERVAL - 1).when(mMockNtpTrustedTime).getCacheAge(); NtpTrustedTime.TimeResult result = mock(NtpTrustedTime.TimeResult.class);
doReturn(MOCK_NTP_TIME).when(mMockNtpTrustedTime).getCachedNtpTime(); doReturn(NtpTimeHelper.NTP_INTERVAL - 1).when(result).getAgeMillis();
doReturn(MOCK_NTP_TIME).when(result).getTimeMillis();
doReturn(result).when(mMockNtpTrustedTime).getCachedTimeResult();
mNtpTimeHelper.retrieveAndInjectNtpTime(); mNtpTimeHelper.retrieveAndInjectNtpTime();
@@ -64,7 +67,9 @@ public class NtpTimeHelperTest {
@Test @Test
public void handleInjectNtpTime_injectTimeFailed_injectTimeDelayed() public void handleInjectNtpTime_injectTimeFailed_injectTimeDelayed()
throws InterruptedException { throws InterruptedException {
doReturn(NtpTimeHelper.NTP_INTERVAL + 1).when(mMockNtpTrustedTime).getCacheAge(); NtpTrustedTime.TimeResult result1 = mock(NtpTrustedTime.TimeResult.class);
doReturn(NtpTimeHelper.NTP_INTERVAL + 1).when(result1).getAgeMillis();
doReturn(result1).when(mMockNtpTrustedTime).getCachedTimeResult();
doReturn(false).when(mMockNtpTrustedTime).forceRefresh(); doReturn(false).when(mMockNtpTrustedTime).forceRefresh();
mNtpTimeHelper.retrieveAndInjectNtpTime(); mNtpTimeHelper.retrieveAndInjectNtpTime();
@@ -72,8 +77,10 @@ public class NtpTimeHelperTest {
assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isFalse(); assertThat(mCountDownLatch.await(2, TimeUnit.SECONDS)).isFalse();
doReturn(true).when(mMockNtpTrustedTime).forceRefresh(); doReturn(true).when(mMockNtpTrustedTime).forceRefresh();
doReturn(1L).when(mMockNtpTrustedTime).getCacheAge(); NtpTrustedTime.TimeResult result2 = mock(NtpTrustedTime.TimeResult.class);
doReturn(MOCK_NTP_TIME).when(mMockNtpTrustedTime).getCachedNtpTime(); doReturn(1L).when(result2).getAgeMillis();
doReturn(MOCK_NTP_TIME).when(result2).getTimeMillis();
doReturn(result2).when(mMockNtpTrustedTime).getCachedTimeResult();
SystemClock.sleep(NtpTimeHelper.RETRY_INTERVAL); SystemClock.sleep(NtpTimeHelper.RETRY_INTERVAL);
waitForTasksToBePostedOnHandlerAndRunThem(); waitForTasksToBePostedOnHandlerAndRunThem();