Java & C# implementation of TOTP: Time-Based One-Time Password Algorithm
/* * Copyright (c) 2018-2019 yingtingxu(徐应庭). All rights reserved. */packagecom.arch.totp;
importjavax.crypto.Mac;
importjavax.crypto.spec.SecretKeySpec;
importjava.nio.ByteBuffer;
importjava.nio.charset.StandardCharsets;
importjava.security.GeneralSecurityException;
/** * The implementation of TOTP: Time-Based One-Time Password Algorithm * <p> * see: * TOTP: https://tools.ietf.org/html/rfc6238 */publicclassTotp {
/** * TOTP supported hash algorithms */publicenumHashAlgorithm {
HmacSHA1("HmacSHA1"), HmacSHA256("HmacSHA256"), HmacSHA512("HmacSHA512");
privateStringname;
HashAlgorithm(Stringname) {
this.name = name;
}
publicStringgetName() {
returnname;
}
@OverridepublicStringtoString() {
returngetName();
}
}
//region public default setting// default hash algorithmpublicstaticfinalHashAlgorithmDEFAULT_HASH_ALGORITHM = HashAlgorithm.HmacSHA1;
// default time step in secondspublicstaticfinalintDEFAULT_TIME_STEP = 30;
// default number of digitspublicstaticfinalintDEFAULT_DIGITS = 8;
// T0 is the Unix time to start counting time steps// (default value is 0, i.e., the Unix epoch)publicstaticfinalintDEFAULT_T0 = 0;
//endregion//region private membersprivatestaticfinalint[] DIGITS_POWER// 0 1 2 3 4 5 6 7 8
= {1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
privateinttimeStep;
privateintt0;
privateintdigits;
privateHashAlgorithmhashAlgorithm;
privateTotp() {
this.timeStep = DEFAULT_TIME_STEP;
this.t0 = DEFAULT_T0;
this.digits = DEFAULT_DIGITS;
this.hashAlgorithm = DEFAULT_HASH_ALGORITHM;
}
//endregion//region builder methodspublicstaticTotpwithDefault() {
returnnewTotp();
}
publicTotptimeStep(inttimeStep) {
this.timeStep = timeStep;
returnthis;
}
publicTotpepoch(intt0) {
this.t0 = t0;
returnthis;
}
publicTotpalgorithm(HashAlgorithmalgorithm) {
this.hashAlgorithm = algorithm;
returnthis;
}
publicTotpdigits(intdigits) {
this.digits = digits;
returnthis;
}
//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 */publicStringgenerateTotp(Stringsecret) {
returngenerateTotp(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 */privateStringgenerateTotp(Stringsecret, longtimeStepNumber) {
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 longbyte[] hmacHash = getHmacSHA(secretBytes, timeStepBytes);
// put selected bytes into result intintoffset = hmacHash[hmacHash.length - 1] & 0xf;
inttruncatedHash = ((hmacHash[offset] & 0x7f) << 24) |
((hmacHash[offset + 1] & 0xff) << 16) |
((hmacHash[offset + 2] & 0xff) << 8) |
(hmacHash[offset + 3] & 0xff);
intotp = truncatedHash % DIGITS_POWER[digits];
Stringresult = Integer.toString(otp);
while (result.length() < digits) {
result = "0" + result;
}
returnresult;
}
/** * 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 */publicbooleanvalidateTotp(Stringsecret, Stringtotp) {
if (!StringUtils.hasText(secret) || !StringUtils.hasText(totp)) {
returnfalse;
}
// Allow a variance of no greater than 90 seconds in either directionlongtimeStepNumber = getCurrentTimeStepNumber();
for (inti = -2; i <= 2; i++) {
Stringtotp2 = generateTotp(secret, timeStepNumber + i);
if (totp.equals(totp2)) {
returntrue;
}
}
// No matchreturnfalse;
}
privatelonggetCurrentTimeStepNumber() {
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 */privatebyte[] getHmacSHA(byte[] secretBytes, byte[] textBytes) {
try {
Machmac = Mac.getInstance(hashAlgorithm.getName());
SecretKeySpecspec = newSecretKeySpec(secretBytes, "RAW");
hmac.init(spec);
returnhmac.doFinal(textBytes);
} catch (GeneralSecurityExceptiongse) {
thrownewIllegalStateException(gse);
}
}
}// Copyright (c) 2018-2019 yingtingxu(徐应庭). All rights reserved.usingSystem;usingSystem.Diagnostics;usingSystem.Net;usingSystem.Security.Cryptography;usingSystem.Text;namespaceArch.Core{/// <summary>/// https://tools.ietf.org/html/rfc6238/// </summary>publicstaticclassTotp{privatestaticreadonlyDateTime_unixEpoch=newDateTime(1970,1,1,0,0,0,DateTimeKind.Utc);privatestaticTimeSpan_timestep=TimeSpan.FromSeconds(30);privatestaticreadonlyEncoding_encoding=newUTF8Encoding(false,true);privatestaticintComputeTotp(HashAlgorithmhashAlgorithm,ulongtimestepNumber,stringmodifier){// # of 0's = length of pinconstintMod=1000000;// See https://tools.ietf.org/html/rfc4226// We can add an optional modifiervartimestepAsBytes=BitConverter.GetBytes(IPAddress.HostToNetworkOrder((long)timestepNumber));varhash=hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes,modifier));// Generate DT stringvaroffset=hash[hash.Length-1]&0xf;Debug.Assert(offset+4<hash.Length);varbinaryCode=(hash[offset]&0x7f)<<24|(hash[offset+1]&0xff)<<16|(hash[offset+2]&0xff)<<8|(hash[offset+3]&0xff);returnbinaryCode%Mod;}privatestaticbyte[]ApplyModifier(byte[]input,stringmodifier){if(string.IsNullOrEmpty(modifier)){returninput;}varmodifierBytes=_encoding.GetBytes(modifier);varcombined=newbyte[checked(input.Length+modifierBytes.Length)];Buffer.BlockCopy(input,0,combined,0,input.Length);Buffer.BlockCopy(modifierBytes,0,combined,input.Length,modifierBytes.Length);returncombined;}// More info: https://tools.ietf.org/html/rfc6238#section-4privatestaticulongGetCurrentTimeStepNumber(){vardelta=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>publicstaticintGenerateTotp(byte[]securityToken,stringmodifier=null){if(securityToken==null){thrownewArgumentNullException(nameof(securityToken));}// Allow a variance of no greater than 90 seconds in either directionvarcurrentTimeStep=GetCurrentTimeStepNumber();using(varhashAlgorithm=newHMACSHA1(securityToken)){returnComputeTotp(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>publicstaticboolValidateTotp(byte[]securityToken,intcode,stringmodifier=null){if(securityToken==null){thrownewArgumentNullException(nameof(securityToken));}// Allow a variance of no greater than 90 seconds in either directionvarcurrentTimeStep=GetCurrentTimeStepNumber();using(varhashAlgorithm=newHMACSHA1(securityToken)){for(vari=-2;i<=2;i++){varcomputedTotp=ComputeTotp(hashAlgorithm,(ulong)((long)currentTimeStep+i),modifier);if(computedTotp==code){returntrue;}}}// No matchreturnfalse;}/// <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>publicstaticintGenerateTotp(stringsecurityToken,stringmodifier=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>publicstaticboolValidateTotp(stringsecurityToken,intcode,stringmodifier=null)=>ValidateCode(Encoding.Unicode.GetBytes(securityToken),code,modifier);}}