Java & C# implementation of TOTP: Time-Based One-Time Password Algorithm
/*
* 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);
}
}
}
// 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);
}
}