Skip to content

Instantly share code, notes, and snippets.

@derlin
Created October 8, 2024 06:02
Show Gist options
  • Save derlin/4a1c3369cfff6cb96c85f299b1a7b8d8 to your computer and use it in GitHub Desktop.
Save derlin/4a1c3369cfff6cb96c85f299b1a7b8d8 to your computer and use it in GitHub Desktop.
A simple java class to generate TOTP codes given a secret key
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