Created
October 8, 2024 06:02
-
-
Save derlin/4a1c3369cfff6cb96c85f299b1a7b8d8 to your computer and use it in GitHub Desktop.
A simple java class to generate TOTP codes given a secret key
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.divio.keycloak.e2e.utils; | |
import java.nio.ByteBuffer; | |
import java.util.Arrays; | |
import javax.crypto.Mac; | |
import javax.crypto.spec.SecretKeySpec; | |
/** | |
* A simple TOTP (Time-based One-Time Password) generator, based on the algorithm described in RFC 6238. | |
* Should be compatible with Google Authenticator and other TOTP-based authenticator apps. | |
* | |
* @see https://www.rfc-editor.org/rfc/rfc6238 | |
*/ | |
public class TotpGenerator { | |
private static final int TOTP_CODE_DIGITS = 6; | |
private static final int TIME_STEP_MILLIS = 30 * 1000; | |
private static final String HMAC_ALGORITHM = "HmacSHA1"; | |
private Mac mac; | |
/** | |
* Base32 is used by Google Authenticator, 1password, etc. for presenting secrets in a human-readable format. For example: | |
* "NRKW I3JT JRLF OR3L G5JH OSKX JVJW CU2N" | |
*/ | |
final class Base32Decoder { | |
// Base32 alphabet (RFC 4648) | |
private static final String BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; | |
private static final int[] BASE32_LOOKUP = new int[256]; | |
static { | |
Arrays.fill(BASE32_LOOKUP, -1); | |
for (int i = 0; i < BASE32_CHARS.length(); i++) { | |
BASE32_LOOKUP[BASE32_CHARS.charAt(i)] = i; | |
} | |
} | |
private Base32Decoder() { | |
} | |
/** | |
* Decode a Base32 string into a byte array. | |
* | |
* @param base32 the human-readable Base32 string (spaces are allowed) | |
* @return the decoded byte array | |
*/ | |
public static byte[] decode(String base32) { | |
// Remove spaces and convert to upper case | |
base32 = base32.replace(" ", "").toUpperCase(); | |
int outputLength = base32.length() * 5 / 8; // 5 bits per character, 8 bits per byte | |
byte[] result = new byte[outputLength]; | |
int buffer = 0; | |
int bitsLeft = 0; | |
int index = 0; | |
for (char c : base32.toCharArray()) { | |
int lookup = BASE32_LOOKUP[c]; | |
if (lookup == -1) { | |
throw new IllegalArgumentException("Invalid Base32 character: " + c); | |
} | |
buffer = (buffer << 5) | lookup; | |
bitsLeft += 5; | |
if (bitsLeft >= 8) { | |
result[index++] = (byte) ((buffer >> (bitsLeft - 8)) & 0xFF); | |
bitsLeft -= 8; | |
} | |
} | |
return result; | |
} | |
} | |
/** | |
* Initialize the Mac instance with the given secret key. | |
*/ | |
public TotpGenerator(String secretKey) throws Exception { | |
// Decode the base64 encoded key | |
byte[] keyBytes = Base32Decoder.decode(secretKey); | |
// Create a HMAC-SHA1 instance and initialize it with the secret key | |
mac = Mac.getInstance(HMAC_ALGORITHM); | |
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, HMAC_ALGORITHM); | |
mac.init(keySpec); | |
} | |
/** | |
* Generate a TOTP code based on the current time. | |
* | |
* @return the 6-digit TOTP code, e.g. "123456" | |
*/ | |
public String generateTOTP() { | |
// Calculate the number of time steps since the Unix epoch | |
long timeStep = System.currentTimeMillis() / TIME_STEP_MILLIS; | |
// Convert time step to byte array | |
byte[] data = ByteBuffer.allocate(8).putLong(timeStep).array(); | |
// Compute the HMAC on the time step data | |
byte[] hmacResult = mac.doFinal(data); | |
// Extract the dynamic offset from the last byte | |
int offset = hmacResult[hmacResult.length - 1] & 0xF; | |
// Extract the 4 bytes starting at the offset and apply the truncation function | |
int binaryCode = ((hmacResult[offset] & 0x7F) << 24) | | |
((hmacResult[offset + 1] & 0xFF) << 16) | | |
((hmacResult[offset + 2] & 0xFF) << 8) | | |
(hmacResult[offset + 3] & 0xFF); | |
// Generate the TOTP code by reducing the binary code to a 6-digit number | |
int totpCode = binaryCode % (int) Math.pow(10, TOTP_CODE_DIGITS); | |
// Return the code, zero-padded to 6 digits | |
return String.format("%06d", totpCode); | |
} | |
public static void main(String[] args) { | |
try { | |
// Replace with your base64 encoded secret key | |
String secretKey = "NRKW I3JT JRLF OR3L G5JH OSKX JVJW CU2N"; | |
TotpGenerator generator = new TotpGenerator(secretKey); | |
// Generate and print the TOTP code | |
String totpCode = generator.generateTOTP(); | |
System.out.println("TOTP Code: " + totpCode); | |
} catch (Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment