I've had major issues with time drift and Time Based One Time Password (TOTP) generation against Google Authenticator.
The android app does something like this to synchronise time, but can only provide an offset down to the nearest second:
public async static Task<long> GetTimeOffsetHTTP(string url = "http://www.google.com") { using (HttpClient HC = new HttpClient()) { HttpResponseMessage Result = await HC.SendAsync(new HttpRequestMessage(HttpMethod.Head, url)); if (Result.Headers.Date.HasValue) return (long)((Result.Headers.Date.Value.Ticks - DateTimeOffset.Now.Ticks) / TimeSpan.TicksPerSecond); } return 0; }
The best example of an SNTP client I could find was the one used by android itself, so with a bit of creativity this was born.
Gist available at https://gist.github.com/BravoTango86/2e221d6cac22f7e432c187c941b01648
/* * Derived from https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/net/SntpClient.java * * Copyright (C) 2016 BravoTango86 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; public class SntpClient { public string DefaultHostName { get; set; } = "time1.google.com"; public int DefaultPort { get; set; } = 123; public int DefaultTimeout { get; set; } = 1000; public bool Debug { get; set; } = true; private const int REFERENCE_TIME_OFFSET = 16; private const int ORIGINATE_TIME_OFFSET = 24; private const int RECEIVE_TIME_OFFSET = 32; private const int TRANSMIT_TIME_OFFSET = 40; private const int NTP_PACKET_SIZE = 48; private const int NTP_PORT = 123; private const int NTP_MODE_CLIENT = 3; private const int NTP_MODE_SERVER = 4; private const int NTP_MODE_BROADCAST = 5; private const int NTP_VERSION = 3; private const int NTP_LEAP_NOSYNC = 3; private const int NTP_STRATUM_DEATH = 0; private const int NTP_STRATUM_MAX = 15; // Number of seconds between Jan 1, 1900 and Jan 1, 1970 // 70 years plus 17 leap days private const long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L; // system time computed from NTP server response public long NTPTime { get; private set; } // value of SystemClock.elapsedRealtime() corresponding to mNtpTime public long NTPTimeReference { get; private set; } public long NTPOffset { get; private set; } // round trip time in milliseconds public long RoundTripTime { get; private set; } private class InvalidServerReplyException : Exception { public InvalidServerReplyException(string message) : base(message) { } } public async Task<bool> RequestTime(IPEndPoint endPoint, int? timeout = null) => await DoRequestTime(endPoint: endPoint, timeout: timeout); public async Task<bool> RequestTime(string hostName, int? port = 123, int? timeout = null) => await DoRequestTime(hostName: hostName, port: port, timeout: timeout); public async Task<bool> RequestTime(int? timeout = null) => await RequestTime(DefaultHostName, timeout); private async Task<bool> DoRequestTime(IPEndPoint endPoint = null, string hostName = null, int? port = null, int? timeout = null) { UdpClient client = null; try { if (endPoint == null && string.IsNullOrEmpty(hostName)) throw new ArgumentException("No destination specified"); using (client = new UdpClient()) { byte[] buffer = new byte[NTP_PACKET_SIZE]; // set mode = 3 (client) and version = 3 // mode is in low 3 bits of first byte // version is in bits 3-5 of first byte buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3); // get current time and write it to the request packet long requestTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); long requestTicks = Environment.TickCount; writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime); if (endPoint != null) await client.SendAsync(buffer, buffer.Length, endPoint); else await client.SendAsync(buffer, buffer.Length, hostName, port ?? 123); // No point in using this, won't timeout // client.Client.ReceiveTimeout = timeout ?? DefaultTimeout; // buffer = (await client.ReceiveAsync()).Buffer; // Messy, not sure how well this works but waiting perpetually for data is hardly efficient... Task<UdpReceiveResult> Receiver = client.ReceiveAsync(); if (Task.WaitAny(Task.Delay(timeout ?? DefaultTimeout), Receiver) == 0) { if (Debug) Console.WriteLine("Timed out on receive"); return false; } else buffer = Receiver.Result.Buffer; long responseTicks = Environment.TickCount; long responseTime = requestTime + (responseTicks - requestTicks); // extract the results byte leap = (byte)((buffer[0] >> 6) & 0x3); byte mode = (byte)(buffer[0] & 0x7); int stratum = (int)(buffer[1] & 0xff); long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET); long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET); long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET); /* do sanity check according to RFC */ // TODO: validate originateTime == requestTime. checkValidServerReply(leap, mode, stratum, transmitTime); long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime); // receiveTime = originateTime + transit + skew // responseTime = transmitTime + transit - skew // clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2 // = ((originateTime + transit + skew - originateTime) + // (transmitTime - (transmitTime + transit - skew)))/2 // = ((transit + skew) + (transmitTime - transmitTime - transit + skew))/2 // = (transit + skew - transit + skew)/2 // = (2 * skew)/2 = skew long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2; if (Debug) Console.WriteLine("round trip: {0}ms clock offset: {1}ms", roundTripTime, clockOffset); // save our results - use the times on this side of the network latency // (response rather than request time) NTPTime = responseTime + clockOffset; NTPTimeReference = responseTicks; NTPOffset = clockOffset; RoundTripTime = roundTripTime; } } catch (Exception e) { if (Debug) Console.WriteLine("request time failed: {0}", e); return false; } return true; } /// <summary> /// Provides the current <see cref="NTPTime"/> as a DateTimeOffset relative to local time or UTC /// </summary> /// <param name="local">Uses local TimeZoneInfo if true else defaults to UTC</param> public DateTimeOffset GetDateTimeOffset(bool local = false) => TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(NTPTime), local ? TimeZoneInfo.Local : TimeZoneInfo.Utc); /// <summary> /// Provides the current NTPOffset as a TimeSpan /// </summary> public TimeSpan GetOffset() => TimeSpan.FromMilliseconds(NTPOffset); private static void checkValidServerReply(byte leap, byte mode, int stratum, long transmitTime) { if (leap == NTP_LEAP_NOSYNC) { throw new InvalidServerReplyException("unsynchronized server"); } if ((mode != NTP_MODE_SERVER) && (mode != NTP_MODE_BROADCAST)) { throw new InvalidServerReplyException("untrusted mode: " + mode); } if ((stratum == NTP_STRATUM_DEATH) || (stratum > NTP_STRATUM_MAX)) { throw new InvalidServerReplyException("untrusted stratum: " + stratum); } if (transmitTime == 0) { throw new InvalidServerReplyException("zero transmitTime"); } } /** * Reads an unsigned 32 bit big endian number from the given offset in the buffer. */ private long read32(byte[] buffer, int offset) { byte b0 = buffer[offset]; byte b1 = buffer[offset + 1]; byte b2 = buffer[offset + 2]; byte b3 = buffer[offset + 3]; // convert signed bytes to unsigned values int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0); int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1); int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2); int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3); return ((long)i0 << 24) + ((long)i1 << 16) + ((long)i2 << 8) + (long)i3; } /** * Reads the NTP time stamp at the given offset in the buffer and returns * it as a system time (milliseconds since January 1, 1970). */ private long readTimeStamp(byte[] buffer, int offset) { long seconds = read32(buffer, offset); long fraction = read32(buffer, offset + 4); // Special case: zero means zero. if (seconds == 0 && fraction == 0) { return 0; } return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L); } /** * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp * at the given offset in the buffer. */ private void writeTimeStamp(byte[] buffer, int offset, long time) { // Special case: zero means zero. if (time == 0) { //Arrays.fill(buffer, offset, offset + 8, (byte)0x00); Buffer.BlockCopy(new byte[8], 0, buffer, offset, 8); return; } long seconds = time / 1000L; long milliseconds = time - seconds * 1000L; seconds += OFFSET_1900_TO_1970; // write seconds in big endian format buffer[offset++] = (byte)(seconds >> 24); buffer[offset++] = (byte)(seconds >> 16); buffer[offset++] = (byte)(seconds >> 8); buffer[offset++] = (byte)(seconds >> 0); long fraction = milliseconds * 0x100000000L / 1000L; // write fraction in big endian format buffer[offset++] = (byte)(fraction >> 24); buffer[offset++] = (byte)(fraction >> 16); buffer[offset++] = (byte)(fraction >> 8); // low order bits should be random data buffer[offset++] = (byte)(new Random().Next(255)); } }
No comments:
Post a Comment