Skip to content

Instantly share code, notes, and snippets.

@Alceatraz
Last active March 9, 2024 14:32
Show Gist options
  • Save Alceatraz/3ca46097110bc451bd141bb453abd4a2 to your computer and use it in GitHub Desktop.
Save Alceatraz/3ca46097110bc451bd141bb453abd4a2 to your computer and use it in GitHub Desktop.
RFC6238 TOTP in java lib-less only std lib
package org.example;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@SuppressWarnings("unused")
public sealed class TOTP permits TOTP.TOTPServer, TOTP.TOTPClient {
//= ================================================================================================================
//= Remove main method for production usage
//= ================================================================================================================
public static void main(String[] args) {
// 1. Create new 2FA secret
// Save bytes into database better then Base32 secret key to Avoid decode overhead
byte[] secretBytes = TOTP.generateSecret();
{
// 2. Convert key into Base32 string for manual setup
TOTPServer server = TOTP.getServer(secretBytes);
String secretKey = server.getSecretKey();
System.out.println(secretKey);
// 3. Convert key into OTP Auth URL for QRCode generation
String secretURL = server.getOTPAuthURL("BTS-TOTP", "test-001", "BTS-Account");
System.out.println(secretURL);
// Do not use it to generate security tokens for logging into your account. You should never store,
// process or otherwise access the secret key on the same machine that you use for accessing your account.
// Doing so completely defeats the security of two-step verification.
// DO NOT USE THIS IN PRODUCTION DO NOT USE THIS IN PRODUCTION DO NOT USE THIS IN PRODUCTION DO NOT USE THIS IN PRODUCTION
String qrcodeURL = "https://api.qrserver.com/v1/create-qr-code/?size=300x300&ecc=M&margin=50&data=" + URLEncoder.encode(secretURL, StandardCharsets.UTF_8);
System.out.println(qrcodeURL);
// DO NOT USE THIS IN PRODUCTION DO NOT USE THIS IN PRODUCTION DO NOT USE THIS IN PRODUCTION DO NOT USE THIS IN PRODUCTION
// 4. Client scan QRCode and get first passcode
TOTPClient client = TOTP.getClient(secretBytes);
int setupPasscode = client.generatePasscode();
// 5. Server verify the setup passcode
boolean verifyPasscode = server.verifyPasscode(setupPasscode);
System.out.println(setupPasscode + " " + verifyPasscode);
}
//-------------------------------------------------------------------------------------------------------------
{
// some day
// 1. Client generate passcode
TOTPClient client = TOTP.getClient(secretBytes);
int loginPasscode = client.generatePasscode();
// 2. Server verify passcode
TOTPServer server = TOTP.getServer(secretBytes);
boolean verifyPasscode = server.verifyPasscode(loginPasscode);
System.out.println(loginPasscode + " " + verifyPasscode);
}
}
//= ================================================================================================================
//= Remove main method for production usage
//= ================================================================================================================
//= ================================================================================================================
public static byte[] generateSecret() {
return generateSecret(Digest.SHA1);
}
public static byte[] generateSecret(Digest hashMode) {
byte[] secret = new byte[hashMode.length];
SecureRandom secureRandom;
try {
secureRandom = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException exception) {
throw new RuntimeException(exception);
}
secureRandom.nextBytes(secret);
return secret;
}
//= ================================================================================================================
public static TOTPServer getServer(byte[] secret) {
return new TOTPServer(Digest.SHA1, 30, 6, secret, null);
}
public static TOTPServer getServer(String secret) {
return new TOTPServer(Digest.SHA1, 30, 6, Base32L.decode(secret), secret);
}
public static TOTPServer getServer(Digest digest, int period, int digits, byte[] secret) {
return new TOTPServer(digest, period, digits, secret, null);
}
public static TOTPServer getServer(Digest digest, int period, int digits, String secret) {
return new TOTPServer(digest, period, digits, Base32L.decode(secret), secret);
}
//= ================================================================================================================
public static TOTPClient getClient(byte[] secret) {
return new TOTPClient(Digest.SHA1, 30, 6, secret, null);
}
public static TOTPClient getClient(String secret) {
return new TOTPClient(Digest.SHA1, 30, 6, Base32L.decode(secret), secret);
}
public static TOTPClient getClient(Digest digest, int period, int digits, byte[] secret) {
return new TOTPClient(digest, period, digits, secret, null);
}
public static TOTPClient getClient(Digest digest, int period, int digits, String secret) {
return new TOTPClient(digest, period, digits, Base32L.decode(secret), secret);
}
//= ================================================================================================================
protected final Digest digest;
protected final int period;
protected final int digits;
protected final byte[] secret;
protected final String encodedSecret;
protected final int divider;
protected TOTP(Digest digest, int period, int digits, byte[] secret, String encodedSecret) {
this.digest = digest;
this.period = period;
this.digits = digits;
this.secret = secret;
this.encodedSecret = encodedSecret;
// divider = Integer.parseInt("1" + "0".repeat(digits));
divider = (int) Math.pow(10, digits);
}
public enum Digest {
SHA1(20, "SHA1", "HmacSHA1"),
SHA256(32, "SHA256", "HmacSHA256"),
SHA512(64, "SHA512", "HmacSHA512");
public final int length;
public final String algorithm;
public final String cipherName;
Digest(int length, String algorithm, String cipherName) {
this.length = length;
this.algorithm = algorithm;
this.cipherName = cipherName;
}
}
protected int passcode(long epoch) {
long timestamp = epoch / period;
SecretKeySpec key = new SecretKeySpec(secret, digest.cipherName);
Mac mac;
try {
mac = Mac.getInstance(digest.cipherName);
mac.init(key);
} catch (NoSuchAlgorithmException | InvalidKeyException exception) {
throw new RuntimeException(exception);
}
byte[] data = new byte[8];
for (int i = 8; i-- > 0; timestamp = timestamp >>> 8) {
data[i] = (byte) timestamp;
}
byte[] hash = mac.doFinal(data);
int offset = hash[hash.length - 1] & 0xf;
int b1 = (hash[offset++] & 0x7f) << 24;
int b2 = (hash[offset++] & 0xff) << 16;
int b3 = (hash[offset++] & 0xff) << 8;
int b4 = (hash[offset] & 0xff);
return (b1 | b2 | b3 | b4) % divider;
}
//= ================================================================================================================
public static final class TOTPServer extends TOTP {
private String secretKey = null;
private TOTPServer(Digest digest, int period, int digits, byte[] secret, String encodedSecret) {
super(digest, period, digits, secret, encodedSecret);
}
/**
* WARNING: This is not thread safe
* You will only use this once, Thread safe is meaningless
*
* @return Base32 encoded key
*/
public String getSecretKey() {
if (encodedSecret == null) {
if (secretKey == null) {
secretKey = Base32L.encode(secret);
}
return secretKey;
} else {
return encodedSecret;
}
}
/**
* WARNING: This is not thread safe
* You will only use this once, Thread safe is meaningless
*
* @param provider provider
* @param username username
* @param issuer issuer
* @return OTP Auth URL
*/
public String getOTPAuthURL(String provider, String username, String issuer) {
provider = URLEncoder.encode(provider, StandardCharsets.UTF_8);
username = URLEncoder.encode(username, StandardCharsets.UTF_8);
issuer = URLEncoder.encode(issuer, StandardCharsets.UTF_8);
return "otpauth://totp/" + provider + ":" + username +
"?secret=" + getSecretKey() +
"&issuer=" + issuer +
"&digits=" + digits +
"&period=" + period +
"&algorithm=" + digest.algorithm;
}
public boolean verifyPasscode(int passcode) {
return verifyPasscode(passcode, System.currentTimeMillis(), 1);
}
public boolean verifyPasscode(int passcode, long epochMilli) {
return verifyPasscode(passcode, epochMilli, 1);
}
public boolean verifyPasscode(int passcode, long epochMilli, int window) {
long epoch = epochMilli / 1000;
if (passcode == super.passcode(epoch)) {
return true;
} else {
for (long i = epoch; i < epoch + window; i++) {
if (passcode == super.passcode(i)) {
return true;
}
}
for (long i = epoch; i > epoch - window; i--) {
if (passcode == super.passcode(i)) {
return true;
}
}
}
return false;
}
}
public static final class TOTPClient extends TOTP {
private final String FORMAT = "%0" + digits + "d";
private TOTPClient(Digest digest, int period, int digits, byte[] secret, String encodedSecret) {
super(digest, period, digits, secret, encodedSecret);
}
public String generateFormattedPasscode() {
return String.format(FORMAT, generatePasscode());
}
public String generateFormattedPasscode(long epochMilli) {
return String.format(FORMAT, generatePasscode(epochMilli));
}
public int generatePasscode() {
return generatePasscode(System.currentTimeMillis());
}
public int generatePasscode(long epochMilli) {
long epoch = epochMilli / 1000;
return super.passcode(epoch);
}
}
//= ================================================================================================================
private static class Base32L {
private static final char[] MAPPER = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7',
};
private static final int[] LOOKUP = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
private static String encode(byte[] input) {
StringBuilder result = new StringBuilder();
int temp = 0;
int size = 0;
for (byte it : input) {
temp = temp << 8;
temp = temp | it & 0xFF;
size = size + 8;
while (size >= 5) {
int move = size - 5;
int code = temp >> move & 0x1F;
result.append(MAPPER[code]);
size = move;
}
}
int remain = input.length * 8 % 5;
if (remain > 0) {
int width = 8 - remain;
temp = temp << width;
size = size + width;
while (size >= 5) {
int move = size - 5;
int code = temp >> move & 0x1F;
result.append(MAPPER[code]);
size = move;
}
int repeat = 8 - result.length() % 8;
result.append("=".repeat(repeat));
}
return result.toString();
}
private static byte[] decode(String input) {
char[] charArray = input.toCharArray();
byte[] buffer = new byte[charArray.length * 5 / 8];
int length = 0;
int temp = 0;
int size = 0;
for (char it : charArray) {
if (it == '=') break;
temp = temp << 5;
temp = temp | LOOKUP[it];
size = size + 5;
while (size >= 8) {
int move = size - 8;
buffer[length++] = (byte) (temp >> move);
size = move;
}
}
byte[] result = new byte[length];
System.arraycopy(buffer, 0, result, 0, length);
return result;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment