Showing posts with label TOTP. Show all posts
Showing posts with label TOTP. Show all posts

Tuesday, 20 September 2016

C# OTP Implementation with TOTP and HOTP

Sample implementation of HOTP and TOTP One Time Passwords (OTP) in C# with .NET Core

This includes an example of bacis caching which can easily be tied into an IMemoryCache instance for web usage.

Gist available at https://gist.github.com/BravoTango86/9ebb578fa4df3a0ffed28bd634f8f3c0
/*
 * 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.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using static OtpAuthenticator;

public class OtpAuthenticator : IDisposable {

    public ICachingProvider CachingProvider { get; set; }
    public int CodeLength { get; set; } = 6;
    public long HotpCounter { get; set; } = 0;
    public int TotpInterval { get; set; } = 30;
    public long TotpTimeOffsetSeconds { get; set; } = 0;

    private OtpType Type;
    private HMAC HMAC;

    public OtpAuthenticator(OtpType type, OtpAlgorithm algorithm = OtpAlgorithm.SHA1, byte[] key = null) {
        HMAC = GetHMAC(algorithm, key ?? GenerateKey(algorithm));
        Type = type;
    }

    public bool VerifyOtp(string code, byte[] challenge = null, int forward = 1, int back = 1) {
        if (string.IsNullOrEmpty(code) || code.Length != CodeLength || (CachingProvider != null && !CachingProvider.ValidateToken(code))) return false;
        long State = GetState(-back);
        List<string> Tried = new List<string>();
        for (int I = 0; I <= (forward + back); I++) {
            string Code = GetOtp(state: State + I, challenge: challenge);
            Tried.Add(code);
            if (Code == code) {
                if (CachingProvider != null) CachingProvider.CancelTokens(Type, Tried);
                if (Type == OtpType.HOTP) HotpCounter = State + I + 1;
                return true;
            }
        }
        return false;
    }

    private long GetState(int offset) {
        return (Type == OtpType.HOTP ? HotpCounter : (DateTimeOffset.UtcNow.ToUnixTimeSeconds() + TotpTimeOffsetSeconds) / TotpInterval) + offset;
    }

    public string GetOtp(int offset = 0, byte[] challenge = null) {
        return GetOtp(GetState(offset), challenge);
    }

    private string GetOtp(long state, byte[] challenge = null) {
        byte[] Input = BitConverter.GetBytes(state);
        if (BitConverter.IsLittleEndian) Array.Reverse(Input);
        if (challenge != null) {
            Array.Resize(ref Input, Input.Length + challenge.Length);
            Buffer.BlockCopy(challenge, 0, Input, 8, challenge.Length);
        }
        byte[] Hash = HMAC.ComputeHash(Input);
        int offset = Hash[Hash.Length - 1] & 0xf;
        int binary = ((Hash[offset] & 0x7f) << 24) | ((Hash[offset + 1] & 0xff) << 16) |
                        ((Hash[offset + 2] & 0xff) << 8) | (Hash[offset + 3] & 0xff);
        return (binary % (int)Math.Pow(10, CodeLength)).ToString(new string('0', CodeLength));
    }

    public string GetIntegrityValue() {
        return GetOtp(0);
    }

    public static string GetUri(OtpType type, byte[] key, string accountName, string issuer = "", OtpAlgorithm algorithm = OtpAlgorithm.SHA1,
                                        int codeLength = 6, long counter = 0, int period = 30) {
        StringBuilder SB = new StringBuilder();
        SB.AppendFormat("otpauth://{0}/", type.ToString().ToLower());
        if (!string.IsNullOrEmpty(issuer)) SB.AppendFormat("{0}:{1}?issuer={0}&", Uri.EscapeUriString(issuer), Uri.EscapeUriString(accountName));
        else SB.AppendFormat("{0}?", Uri.EscapeUriString(accountName));
        SB.AppendFormat("secret={0}&algorithm={1}&digits={2}&", Base32.Encode(key), algorithm, codeLength);
        if (type == OtpType.HOTP) SB.AppendFormat("counter={0}", counter);
        else SB.AppendFormat("period={0}", period);
        return SB.ToString();
    }

    public string GetUri(string accountName, string issuer = "") {
        return GetUri(Type, HMAC.Key, accountName, issuer = "", (OtpAlgorithm)Enum.Parse(typeof(OtpAlgorithm), HMAC.HashName),
                            CodeLength, HotpCounter, TotpInterval);
    }

    public override string ToString() {
        return GetUri("OtpGenerator");
    }

    public static byte[] GenerateKey(OtpAlgorithm algorithm) {
        return GenerateKey(GetHashLength(algorithm));
    }

    public static byte[] GenerateKey(int length) {
        using (RandomNumberGenerator RNG = RandomNumberGenerator.Create()) {
            byte[] Output = new byte[length];
            RNG.GetBytes(Output);
            return Output;
        }
    }

    public void Dispose() {
        HMAC.Dispose();
    }

    private static HMAC GetHMAC(OtpAlgorithm algorithm, byte[] key) {
        switch (algorithm) {
            case OtpAlgorithm.MD5: return new HMACMD5(key);
            case OtpAlgorithm.SHA1: return new HMACSHA1(key);
            case OtpAlgorithm.SHA256: return new HMACSHA256(key);
            case OtpAlgorithm.SHA512: return new HMACSHA512(key);
        }
        throw new InvalidOperationException();
    }

    private static int GetHashLength(OtpAlgorithm algorithm) {
        switch (algorithm) {
            case OtpAlgorithm.MD5: return 32;
            case OtpAlgorithm.SHA1: return 20;
            case OtpAlgorithm.SHA256: return 32;
            case OtpAlgorithm.SHA512: return 64;
        }
        throw new InvalidOperationException();
    }

}


public enum OtpType {
    HOTP = 0,
    TOTP = 1
}

public enum OtpAlgorithm {
    MD5 = 10,
    SHA1 = 1,
    SHA256 = 2,
    SHA512 = 3
}

public interface ICachingProvider {
    void CancelToken(OtpType type, string token);
    void CancelTokens(OtpType type, IEnumerable<string> tokens);
    bool ValidateToken(string token);
}

public class LocalCachingProvider : ICachingProvider {

    private List<string> Used = new List<string>();

    public void CancelToken(OtpType type, string token) {
        Used.Add(token);
    }

    public void CancelTokens(OtpType type, IEnumerable<string> tokens) {
        Used.AddRange(tokens);
    }

    public bool ValidateToken(string token) {
        return !Used.Contains(token);
    }
}

Borrowing the barcode generator from before...

public static void Main(string[] args) {
    Console.OutputEncoding = Encoding.UTF8;
    Console.WindowWidth = 86;
    Console.WindowHeight = 44;
    StringRenderer Renderer = new StringRenderer() { Block = "  ", Empty = "\u2588\u2588", NewLine = "\n    ", };
    EncodingOptions Options = new EncodingOptions { Height = 0, Width = 0, Margin = 1 };
    using (OtpAuthenticator Authenticator = new OtpAuthenticator(OtpType.TOTP) { CachingProvider = new LocalCachingProvider() }) {
        Console.WriteLine("\n{1}{0}{1}", BarcodeGenerator.Generate(Renderer, Authenticator.GetUri("Test Account", "OTPGenerator"), Options), Renderer.NewLine);
        while (true) {
            string Code = Console.ReadLine();
            if (Authenticator.VerifyOtp(Code)) Console.WriteLine("Code Accepted");
            else Console.WriteLine("Code Invalid");
        }
    }
}

...and scanning the generated barcode with Google Authenticator, we can check it works:

Friday, 16 September 2016

C# SNTP Client based on android.net.SntpClient

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));
    }

}