Compare commits

...

1 Commits
1.6 ... master

Author SHA1 Message Date
46acdedb8b
Add OTP#getNextPin(), clean up code 2024-09-07 19:48:16 +02:00
7 changed files with 126 additions and 142 deletions

View File

@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.cringe_studios</groupId>
<artifactId>CringeAuthenticatorLibrary</artifactId>
<version>1.6</version>
<version>1.7</version>
<name>CringeAuthenticatorLibrary</name>
<description>The Library of the Cringe Authenticator</description>
<build>

View File

@ -1,13 +1,12 @@
package com.cringe_studios.cringe_authenticator_library;
import java.security.InvalidKeyException;
import com.cringe_studios.cringe_authenticator_library.impl.Base32;
import com.cringe_studios.cringe_authenticator_library.impl.TOTP;
public abstract class OTP {
//TOTP
//HOTP
protected byte[] secret;
protected String base32Secret;
protected OTPType type;
@ -17,7 +16,7 @@ public abstract class OTP {
protected long period;
protected boolean checksum;
static protected long timeOffset;
protected OTP(OTPType nType, String nSecret, OTPAlgorithm nAlgorithm, int nDigits, long nCounter, long nPeriodInSeconds, boolean nChecksum) {
this.secret = Base32.decode(nSecret);
base32Secret = nSecret;
@ -28,16 +27,16 @@ public abstract class OTP {
period = (nPeriodInSeconds <= 0) ? 30 : nPeriodInSeconds;
checksum = nChecksum;
}
//secret: required - base32 encoded
//algorithm: optional (default SHA1)
//digits: optional (default 6)
//OTPType: required
//period: optional (default 30)
//counter: required if hotp
/**
*
*
* @param type REQUIRED: the type of the OTP (HTOP or TOTP)
* @param secret REQUIRED: the secret, base32 encoded
* @param algorithm OPTIONAL (default SHA1) the hash algorithm to use (SHA1 SHA224 SHA256 SHA384 SHA512)
@ -46,57 +45,68 @@ public abstract class OTP {
* @param periodInSeconds OPTIONAL (default 30) for TOTP, the refresh rate of the TOTP
* @param checksum OPTIONAL: appends a checksum digit to the end of the string
* @return returns a valid OTP object, which can be used to create a new OTP pin
*/
*/
public static OTP createNewOTP(OTPType type, String secret, OTPAlgorithm algorithm, int digits, long counter, long periodInSeconds, boolean checksum) throws OTPException{
if(type == null) throw new OTPException("No OTP Type given!");
return type.instance(secret, algorithm, digits, counter, periodInSeconds, checksum);
}
public abstract long getNextDueTime();
/**
*
* @return s the current pin, as calculated by the counter or the current time, unless it
* @throws an OTPException, which is either thrown, if you somehow create an OTP with an invalid key, despite the checks in the instantiate method, or if your platform does not support the hashing algorithm you selected
*
* @return the current pin, as calculated by the counter or the current time
* @throws OTPException is either thrown, if you somehow create an OTP with an invalid key, despite the checks in the instantiate method, or if your platform does not support the hashing algorithm you selected
*/
public abstract String getPin() throws OTPException;
/**
*
* @return the next pin, as calculated by the counter or the current time
* @throws OTPException is either thrown, if you somehow create an OTP with an invalid key, despite the checks in the instantiate method, or if your platform does not support the hashing algorithm you selected
*/
public abstract String getNextPin() throws OTPException;
public abstract void incrementCounter();
public String getSecret() {
return this.base32Secret;
}
public OTPType getOTPType() {
return this.type;
}
public OTPAlgorithm getAlgorithm() {
return algorithm;
}
public int getDigits() {
return this.digits;
}
public long getCounter() {
return this.counter;
}
public long getPeriod() {
return this.period;
}
public boolean hasChecksum() {
return checksum;
}
// Seconds towards positive infinity means a positive correction into the future (seconds = 2 -> 5 becomes 7)
// Seconds towards negative infinity means a negative correction into the past (seconds = -2 -> 5 becomes 3)
/**
* Adjusts the timestamps used for OTP calculations in {@link TOTP} codes relative to the system clock.<br>
* - A positive value means a positive correction into the future (seconds = 2 -> 5 becomes 7)<br>
* - A negative value means a negative correction into the past (seconds = -2 -> 5 becomes 3)
* @param seconds The amount to adjust by
*/
public static void setTimeCorrection(long seconds) {
timeOffset = seconds;
}
public static long getTimeCorrection() {
return timeOffset;
}

View File

@ -1,11 +1,9 @@
package com.cringe_studios.cringe_authenticator_library.impl;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import com.cringe_studios.cringe_authenticator_library.OTP;
import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm;
@ -29,18 +27,19 @@ public class HOTP extends OTP {
}
@Override
public String getPin() throws OTPException{
public String getPin() throws OTPException {
return getPinAtCounter(getCounter());
}
@Override
public String getNextPin() throws OTPException {
return getPinAtCounter(getCounter() + 1);
}
public String getPinAtCounter(long counter) throws OTPException {
try {
long movingFactor = this.getCounter();
byte[] text = new byte[Long.BYTES];
for (int i = text.length - 1; i >= 0; i--) {
text[i] = (byte) (movingFactor & 0xff);
movingFactor >>= 8;
}
return generateOTP(-1, text);
byte[] msg = ByteBuffer.allocate(Long.BYTES).putLong(counter).array();
return generateOTP(-1, msg);
} catch (InvalidKeyException e) {
throw new OTPException("Your secret is invalid! Please rescan the code!", e);
} catch (NoSuchAlgorithmException e) {
@ -54,10 +53,10 @@ public class HOTP extends OTP {
int codeDigits = this.getDigits();
// put movingFactor value into text byte array
int digits = addChecksum ? (codeDigits + 1) : codeDigits;
byte[] text = message;
// compute hmac hash
byte[] hashIncorrectLength = this.getAlgorithm().hash(secret, text);
@ -73,16 +72,16 @@ public class HOTP extends OTP {
| ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
//int otp = binary % DIGITS_POWER[codeDigits];
//byte[] truncatedHash = Arrays.copyOfRange(hash, offset, offset + 3);
//truncatedHash[0] &= 0x7F;
//BigInteger bigBinary = new BigInteger(1, truncatedHash);
BigInteger bigBinary = BigInteger.valueOf(binary);
BigInteger bigOtp = bigBinary.mod(BigInteger.TEN.pow(codeDigits));
//System.out.println(binary + " vs. " + bigBinary.toString());
//System.out.println(otp + " vs. " + bigOtp.toString());
if (addChecksum) {
//otp = (otp * 10) + HOTPChecksumProvider.calcChecksum(otp, codeDigits);
bigOtp = bigOtp.multiply(BigInteger.TEN).add(BigInteger.valueOf(HOTPChecksumProvider.calcChecksum(bigOtp.longValue(), codeDigits)));

View File

@ -1,21 +1,17 @@
package com.cringe_studios.cringe_authenticator_library.impl;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm;
import com.cringe_studios.cringe_authenticator_library.OTPException;
import com.cringe_studios.cringe_authenticator_library.OTPType;
//https://datatracker.ietf.org/doc/html/rfc6238
public class TOTP extends HOTP {
public TOTP(String secret, OTPAlgorithm algorithm, int digits, long counter, long periodInSeconds, boolean checksum) {
super(secret, algorithm, digits, counter, periodInSeconds, checksum);
this.type = OTPType.TOTP;
}
@Override
public long getNextDueTime() {
// e.g. current time: 20 seconds & current period: 30 seconds -> next due time:
@ -24,50 +20,23 @@ public class TOTP extends HOTP {
}
@Override
public String getPin() throws OTPException{
try {
return getPinAt(System.currentTimeMillis() / 1000);
} catch (InvalidKeyException e) {
throw new OTPException("Your secret is invalid! Please rescan the code!", e);
} catch (NoSuchAlgorithmException e) {
throw new OTPException("Your platform does not support the " + this.getAlgorithm().name() + "algorithm!", e);
}
public String getPin() throws OTPException {
return getPinAtTime(System.currentTimeMillis() / 1000);
}
@Override
public long getCounter() {
return getCounterAt(System.currentTimeMillis() / 1000 - super.getTimeCorrection());
return getCounterAt(System.currentTimeMillis() / 1000);
}
private long getCounterAt(long unixSecond) {
return unixSecond / this.getPeriod();
return (unixSecond - getTimeCorrection()) / this.getPeriod();
}
public String getPinAt(long time) throws InvalidKeyException, NoSuchAlgorithmException {
//int codeDigits = this.getDigits();
// Get the HEX in a Byte[]
byte[] msg = ByteBuffer.allocate(Long.BYTES).putLong(this.getCounterAt(time)).array();
//byte[] k = this.secret;
//byte[] hash = this.getAlgorithm().hash(k, msg);
// put selected bytes into result int
//int offset = hash[hash.length - 1] & 0xf;
//int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16)
// | ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
//int otp = binary % DIGITS_POWER[codeDigits];
//String result = Integer.toString(otp);
//while (result.length() < codeDigits) {
// result = "0" + result;
//}
//return result;
return this.generateOTP(-1, msg);
public String getPinAtTime(long unixSecond) throws OTPException {
return getPinAtCounter(getCounterAt(unixSecond));
}
@Override
public void incrementCounter() {}
}

View File

@ -18,6 +18,7 @@ import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm;
import com.cringe_studios.cringe_authenticator_library.OTPException;
import com.cringe_studios.cringe_authenticator_library.OTPType;
import com.cringe_studios.cringe_authenticator_library.impl.Base32;
import com.cringe_studios.cringe_authenticator_library.impl.HOTP;
import com.cringe_studios.cringe_authenticator_library.impl.TOTP;
public class OTPTest {
@ -32,34 +33,34 @@ public class OTPTest {
String[] expectedValues = { "755224", "287082", "359152", "969429", "338314", "254676", "287922", "162583",
"399871", "520489" };
if(VERBOSE) {
System.out.println("calcPin expected");
}
for (int i = 0; i < 10; i++) {
int tempi = i;
assertDoesNotThrow(() ->{
assertDoesNotThrow(() ->{
OTP testOTP1 = OTPType.HOTP.instance(base32secret, OTPAlgorithm.SHA1, 6, tempi, 30, false);
String calculatedPin = testOTP1.getPin();
if(VERBOSE) {
System.out.println(calculatedPin + " == " + expectedValues[tempi]);
}
assertEquals(calculatedPin, expectedValues[tempi]);
});
}
}
@Test
public void md5Test() {
String secret = String.format("%x", new BigInteger(1, "MD5".getBytes()));
String base32secret = Base32.encode(hexStr2Bytes(secret));
//System.out.println("MD5 String: " + base32secret);
assertDoesNotThrow(() -> {
OTP testOTP1 = OTPType.HOTP.instance(base32secret, OTPAlgorithm.MD5, 6, 0, 30, false);
for(int i = 0; i < 5; i++) {
@ -69,10 +70,10 @@ public class OTPTest {
System.out.println("Calculated MD5 pin - counter: " + i + " pin: " + calculatedPin);
}
}
});
}
@Test
public void lengthTest() {
assertDoesNotThrow(() -> {
@ -80,41 +81,41 @@ public class OTPTest {
String base32secret = Base32.encode(hexStr2Bytes(secret));
OTP testOTP1 = OTP.createNewOTP(OTPType.HOTP, base32secret, OTPAlgorithm.SHA1, 1, 0, 0, false);
assertDoesNotThrow(() -> testOTP1.getPin());
secret = "100";
base32secret = Base32.encode(hexStr2Bytes(secret));
OTP testOTP2 = OTP.createNewOTP(OTPType.HOTP, base32secret, OTPAlgorithm.SHA256, 0, 0, 0, false);
assertDoesNotThrow(() -> testOTP2.getPin());
OTP testOTP3 = OTP.createNewOTP(OTPType.HOTP, base32secret, OTPAlgorithm.SHA512, 50, 0, 0, false);
assertDoesNotThrow(() -> testOTP3.getPin());
OTP testOTP4 = OTP.createNewOTP(OTPType.HOTP, base32secret, OTPAlgorithm.SHA512, 10, 0, 0, false);
assertDoesNotThrow(() -> testOTP4.getPin());
assertThrows(OTPException.class, () -> {
OTP testOTP5 = OTP.createNewOTP(OTPType.HOTP, "", OTPAlgorithm.SHA1, 8, 0, 0, false);
assertNotNull(testOTP5);
});
});
}
@Test
public void meeemstersCrashTest() {
assertDoesNotThrow(() -> {
String secret = "s3ydtxo35tz3n3ob5dehlh76ciokhmsbbae5zusz4vm3k6wxmt6lsfvt";
//String base32secret = Base32.encode(hexStr2Bytes(secret));
OTP testOTP1 = OTP.createNewOTP(OTPType.HOTP, secret, OTPAlgorithm.MD5, 12, 0, 1, true);
assertDoesNotThrow(() -> testOTP1.getPin());
OTP testOTP2 = OTP.createNewOTP(OTPType.TOTP, secret, OTPAlgorithm.MD5, 12, 0, 1, true);
for(int i = 0; i < 1000; i++) {
assertDoesNotThrow(() -> testOTP2.getPin());
testOTP2.incrementCounter();
}
//OTP testOTP2 = OTP.createNewOTP(OTPType.HOTP, Base32.encode(hexStr2Bytes(secret)), OTPAlgorithm.MD5, 12, 0, 1, true);
//assertDoesNotThrow(() -> testOTP2.getPin());
});
@ -131,7 +132,7 @@ public class OTPTest {
ret[i] = bArray[i + 1];
return ret;
}
@Test
public void totpTest() {
String expectedValues[] = {
@ -154,7 +155,7 @@ public class OTPTest {
"77737706",
"47863826"
};
// Seed for HMAC-SHA1 - 20 bytes
String base16seed = "3132333435363738393031323334353637383930";
// Seed for HMAC-SHA256 - 32 bytes
@ -167,45 +168,45 @@ public class OTPTest {
String seed64 = Base32.encode(hexStr2Bytes(base16seed64));
long testTime[] = { 59L, 1111111109L, 1111111111L, 1234567890L, 2000000000L, 20000000000L };
assertDoesNotThrow(() -> {
TOTP testOTP1 = (TOTP) OTPType.TOTP.instance(seed, OTPAlgorithm.SHA1, 8, 0, 30, false);
TOTP testOTP256 = (TOTP) OTPType.TOTP.instance(seed32, OTPAlgorithm.SHA256, 8, 0, 30, false);
TOTP testOTP512 = (TOTP) OTPType.TOTP.instance(seed64, OTPAlgorithm.SHA512, 8, 0, 30, false);
long X = 30;
String steps = "0";
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
try {
if(VERBOSE) {
System.out.println("+---------------+-----------------------+--------+--------+");
System.out.println("| Time(sec) | Time (UTC format) | TOTP | Mode |");
System.out.println("+---------------+-----------------------+--------+--------+");
}
for (int i = 0; i < testTime.length; i++) {
String fmtTime = String.format("%1$-11s", testTime[i]);
String utcTime = df.format(new Date(testTime[i] * 1000));
String sha1Pin = testOTP1.getPinAt(testTime[i]);
String sha256Pin = testOTP256.getPinAt(testTime[i]);
String sha512Pin = testOTP512.getPinAt(testTime[i]);
String sha1Pin = testOTP1.getPinAtTime(testTime[i]);
String sha256Pin = testOTP256.getPinAtTime(testTime[i]);
String sha512Pin = testOTP512.getPinAtTime(testTime[i]);
if(VERBOSE) {
System.out.println("| " + fmtTime + " | " + utcTime + " |" + sha1Pin + "| SHA1 |");
System.out.println("| " + fmtTime + " | " + utcTime + " |" + sha256Pin + "| SHA256 |");
System.out.println("| " + fmtTime + " | " + utcTime + " |" + sha512Pin + "| SHA512 |");
System.out.println("+---------------+-----------------------+--------+--------+");
}
assertEquals(expectedValues[i * 3 + 0], sha1Pin);
assertEquals(expectedValues[i * 3 + 1], sha256Pin);
assertEquals(expectedValues[i * 3 + 2], sha512Pin);
@ -216,6 +217,18 @@ public class OTPTest {
});
}
@Test
public void testGetNextPin() {
assertDoesNotThrow(() -> {
String secret = "s3ydtxo35tz3n3ob5dehlh76ciokhmsbbae5zusz4vm3k6wxmt6lsfvt";
HOTP testOTP1 = (HOTP) OTP.createNewOTP(OTPType.HOTP, secret, OTPAlgorithm.SHA1, 12, 0, 0, true);
TOTP testOTP2 = (TOTP) OTP.createNewOTP(OTPType.TOTP, secret, OTPAlgorithm.SHA1, 12, 0, 60, true);
assertEquals(testOTP1.getNextPin(), testOTP1.getPinAtCounter(testOTP1.getCounter() + 1));
assertEquals(testOTP2.getNextPin(), testOTP2.getPinAtTime(System.currentTimeMillis() / 1000 + testOTP2.getPeriod()));
});
}
// @org.junit.jupiter.api.Test
// public void test() {
// hotpTest();

View File

@ -31,16 +31,9 @@
package test;
import java.io.IOException;
import java.io.File;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.lang.reflect.UndeclaredThrowableException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.security.InvalidKeyException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@ -159,7 +152,7 @@ public class ogHOTP {
// compute hmac hash
byte[] hash = hmac_sha1(secret, text);
System.out.print(Arrays.toString(hash) + " ");
// put selected bytes into result int
@ -180,7 +173,7 @@ public class ogHOTP {
}
return result;
}
public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
@ -190,10 +183,10 @@ public class ogHOTP {
}
return data;
}
public static void main(String[] args) {
String Secret = "3132333435363738393031323334353637383930"; // Secret = 0x3132333435363738393031323334353637383930
byte[] byteSecret = hexStringToByteArray(Secret);
for(int i = 0; i < 10; i++) {
try {
@ -204,6 +197,6 @@ public class ogHOTP {
e.printStackTrace();
}
}
}
}

View File

@ -1,16 +1,16 @@
package test;
import java.lang.reflect.UndeclaredThrowableException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.TimeZone;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.TimeZone;
public class ogTOTPmod1 {
@ -75,7 +75,7 @@ public class ogTOTPmod1 {
public static String generateTOTP(String key, long timein, String crypto) {
int codeDigits = 8;
long T = timein / 30;
// Get the HEX in a Byte[]