Skip to content

Latest commit

 

History

History
339 lines (288 loc) · 11.3 KB

README.md

File metadata and controls

339 lines (288 loc) · 11.3 KB

TOTP

Java & C# implementation of TOTP: Time-Based One-Time Password Algorithm

Java

/*
 * Copyright (c) 2018-2019 yingtingxu(徐应庭). All rights reserved.
 */

package com.arch.totp;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;

/**
 * The implementation of TOTP: Time-Based One-Time Password Algorithm
 * <p>
 * see:
 * TOTP: https://tools.ietf.org/html/rfc6238
 */
public class Totp {
    /**
     * TOTP supported hash algorithms
     */
    public enum HashAlgorithm {
        HmacSHA1("HmacSHA1"), HmacSHA256("HmacSHA256"), HmacSHA512("HmacSHA512");

        private String name;

        HashAlgorithm(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return getName();
        }
    }

    //region public default setting
    // default hash algorithm
    public static final HashAlgorithm DEFAULT_HASH_ALGORITHM = HashAlgorithm.HmacSHA1;

    // default time step in seconds
    public static final int DEFAULT_TIME_STEP = 30;

    // default number of digits
    public static final int DEFAULT_DIGITS = 8;

    // T0 is the Unix time to start counting time steps
    // (default value is 0, i.e., the Unix epoch)
    public static final int DEFAULT_T0 = 0;
    //endregion

    //region private members
    private static final int[] DIGITS_POWER
            // 0  1   2    3     4      5       6        7         8
            = {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};

    private int timeStep;
    private int t0;
    private int digits;
    private HashAlgorithm hashAlgorithm;

    private Totp() {
        this.timeStep = DEFAULT_TIME_STEP;
        this.t0 = DEFAULT_T0;
        this.digits = DEFAULT_DIGITS;
        this.hashAlgorithm = DEFAULT_HASH_ALGORITHM;
    }
    //endregion

    //region builder methods
    public static Totp withDefault() {
        return new Totp();
    }

    public Totp timeStep(int timeStep) {
        this.timeStep = timeStep;
        return this;
    }

    public Totp epoch(int t0) {
        this.t0 = t0;
        return this;
    }

    public Totp algorithm(HashAlgorithm algorithm) {
        this.hashAlgorithm = algorithm;
        return this;
    }

    public Totp digits(int digits) {
        this.digits = digits;
        return this;
    }
    //endregion

    /**
     * This method generates a TOTP value for the given secret.
     *
     * @param secret: the shared secret, HEX encoded
     * @return: a numeric String in base 10 that includes truncated digits
     */
    public String generateTotp(String secret) {
        return generateTotp(secret, getCurrentTimeStepNumber());
    }

    /**
     * This method generates a TOTP value for the given secret.
     *
     * @param secret:         the shared secret, HEX encoded
     * @param timeStepNumber: the number of time step between the initial time T0 and the current unix time.
     * @return: a numeric String in base 10 that includes truncated digits
     */
    private String generateTotp(String secret, long timeStepNumber) {
        Assert.hasText(secret, "secret cannot be null or empty");

        // Using the counter
        // First 8 bytes are for the movingFactor
        // Compliant with base RFC 4226 (HOTP)
        byte[] timeStepBytes = ByteBuffer.allocate(8)
                .putLong(timeStepNumber)
                .array();

        byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8);

        // the output would be a 20 byte long
        byte[] hmacHash = getHmacSHA(secretBytes, timeStepBytes);

        // put selected bytes into result int
        int offset = hmacHash[hmacHash.length - 1] & 0xf;

        int truncatedHash = ((hmacHash[offset] & 0x7f) << 24) |
                ((hmacHash[offset + 1] & 0xff) << 16) |
                ((hmacHash[offset + 2] & 0xff) << 8) |
                (hmacHash[offset + 3] & 0xff);

        int otp = truncatedHash % DIGITS_POWER[digits];

        String result = Integer.toString(otp);
        while (result.length() < digits) {
            result = "0" + result;
        }
        return result;
    }

    /**
     * Validates the TOTP for the specified secret
     *
     * @param secret: the shared secret, HEX encoded
     * @param totp:   the TOTP generated by the secret
     * @return {@code true} if the TOTP is valid
     */
    public boolean validateTotp(String secret, String totp) {
        if (!StringUtils.hasText(secret) || !StringUtils.hasText(totp)) {
            return false;
        }

        // Allow a variance of no greater than 90 seconds in either direction
        long timeStepNumber = getCurrentTimeStepNumber();
        for (int i = -2; i <= 2; i++) {
            String totp2 = generateTotp(secret, timeStepNumber + i);
            if (totp.equals(totp2)) {
                return true;
            }
        }

        // No match
        return false;
    }

    private long getCurrentTimeStepNumber() {
        return (System.currentTimeMillis() / 1000L - t0) / timeStep;
    }

    /**
     * This method uses the JCE to provide the crypto algorithm.
     * HMAC computes a Hashed Message Authentication Code with the
     * crypto hash algorithm as a parameter.
     *
     * @param secretBytes: the bytes to use for the HMAC key
     * @param textBytes:   the message or text to be authenticated
     */
    private byte[] getHmacSHA(byte[] secretBytes, byte[] textBytes) {
        try {
            Mac hmac = Mac.getInstance(hashAlgorithm.getName());
            SecretKeySpec spec = new SecretKeySpec(secretBytes, "RAW");
            hmac.init(spec);
            return hmac.doFinal(textBytes);
        } catch (GeneralSecurityException gse) {
            throw new IllegalStateException(gse);
        }
    }
}

C#

// Copyright (c) 2018-2019 yingtingxu(徐应庭). All rights reserved.

using System;
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Text;

namespace Arch.Core
{
    /// <summary>
    /// https://tools.ietf.org/html/rfc6238
    /// </summary>
    public static class Totp
    {
        private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        private static TimeSpan _timestep = TimeSpan.FromSeconds(30);
        private static readonly Encoding _encoding = new UTF8Encoding(false, true);

        private static int ComputeTotp(HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
        {
            // # of 0's = length of pin
            const int Mod = 1000000;

            // See https://tools.ietf.org/html/rfc4226
            // We can add an optional modifier
            var timestepAsBytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));
            var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));

            // Generate DT string
            var offset = hash[hash.Length - 1] & 0xf;
            Debug.Assert(offset + 4 < hash.Length);
            var binaryCode = (hash[offset] & 0x7f) << 24
                             | (hash[offset + 1] & 0xff) << 16
                             | (hash[offset + 2] & 0xff) << 8
                             | (hash[offset + 3] & 0xff);

            return binaryCode % Mod;
        }

        private static byte[] ApplyModifier(byte[] input, string modifier)
        {
            if (string.IsNullOrEmpty(modifier))
            {
                return input;
            }

            var modifierBytes = _encoding.GetBytes(modifier);
            var combined = new byte[checked(input.Length + modifierBytes.Length)];
            Buffer.BlockCopy(input, 0, combined, 0, input.Length);
            Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length);
            return combined;
        }

        // More info: https://tools.ietf.org/html/rfc6238#section-4
        private static ulong GetCurrentTimeStepNumber()
        {
            var delta = DateTime.UtcNow - _unixEpoch;
            return (ulong)(delta.Ticks / _timestep.Ticks);
        }

        /// <summary>
        /// Generates TOTP for the specified <paramref name="securityToken"/>.
        /// </summary>
        /// <param name="securityToken">The security token to generate TOTP.</param>
        /// <param name="modifier">The modifier.</param>
        /// <returns>The generated code.</returns>
        public static int GenerateTotp(byte[] securityToken, string modifier = null)
        {
            if (securityToken == null)
            {
                throw new ArgumentNullException(nameof(securityToken));
            }

            // Allow a variance of no greater than 90 seconds in either direction
            var currentTimeStep = GetCurrentTimeStepNumber();
            using (var hashAlgorithm = new HMACSHA1(securityToken))
            {
                return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
            }
        }

        /// <summary>
        /// Validates the TOTP for the specified <paramref name="securityToken"/>.
        /// </summary>
        /// <param name="securityToken">The security token for verifying.</param>
        /// <param name="code">The TOTP to validate.</param>
        /// <param name="modifier">The modifier</param>
        /// <returns><c>True</c> if validate succeed, otherwise, <c>false</c>.</returns>
        public static bool ValidateTotp(byte[] securityToken, int code, string modifier = null)
        {
            if (securityToken == null)
            {
                throw new ArgumentNullException(nameof(securityToken));
            }

            // Allow a variance of no greater than 90 seconds in either direction
            var currentTimeStep = GetCurrentTimeStepNumber();
            using (var hashAlgorithm = new HMACSHA1(securityToken))
            {
                for (var i = -2; i <= 2; i++)
                {
                    var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier);
                    if (computedTotp == code)
                    {
                        return true;
                    }
                }
            }

            // No match
            return false;
        }

        /// <summary>
        /// Generates TOTP for the specified <paramref name="securityToken"/>.
        /// </summary>
        /// <param name="securityToken">The security token to generate code.</param>
        /// <param name="modifier">The modifier.</param>
        /// <returns>The generated code.</returns>
        public static int GenerateTotp(string securityToken, string modifier = null) => GenerateCode(Encoding.Unicode.GetBytes(securityToken), modifier);

        /// <summary>
        /// Validates the TOTP for the specified <paramref name="securityToken"/>.
        /// </summary>
        /// <param name="securityToken">The security token for verifying.</param>
        /// <param name="code">The code to validate.</param>
        /// <param name="modifier">The modifier</param>
        /// <returns><c>True</c> if validate succeed, otherwise, <c>false</c>.</returns>
        public static bool ValidateTotp(string securityToken, int code, string modifier = null) => ValidateCode(Encoding.Unicode.GetBytes(securityToken), code, modifier);
    }
}