Skip to content

Commit ceb19dd

Browse files
committed
Add TOTP one-time password generation
1 parent 2f556a0 commit ceb19dd

File tree

5 files changed

+166
-0
lines changed

5 files changed

+166
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright (c) 2019 Sam Stevens
3+
*
4+
* Licensed under the MIT License
5+
*
6+
* https://github.com/samdjstevens v1.7.1
7+
*/
8+
package org.labkey.remoteapi.totp;
9+
10+
public class CodeGenerationException extends Exception {
11+
public CodeGenerationException(String message, Throwable cause) {
12+
super(message, cause);
13+
}
14+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (c) 2019 Sam Stevens
3+
*
4+
* Licensed under the MIT License
5+
*
6+
* https://github.com/samdjstevens v1.7.1
7+
*/
8+
package org.labkey.remoteapi.totp;
9+
10+
import org.apache.commons.codec.binary.Base32;
11+
12+
import javax.crypto.Mac;
13+
import javax.crypto.spec.SecretKeySpec;
14+
import java.security.InvalidKeyException;
15+
import java.security.InvalidParameterException;
16+
import java.security.NoSuchAlgorithmException;
17+
18+
public class CodeGenerator
19+
{
20+
private final HashingAlgorithm algorithm;
21+
private final int digits;
22+
23+
public CodeGenerator(HashingAlgorithm algorithm, int digits) {
24+
if (algorithm == null) {
25+
throw new InvalidParameterException("HashingAlgorithm must not be null.");
26+
}
27+
if (digits < 1) {
28+
throw new InvalidParameterException("Number of digits must be higher than 0.");
29+
}
30+
31+
this.algorithm = algorithm;
32+
this.digits = digits;
33+
}
34+
35+
public String generate(String key, long counter) throws CodeGenerationException {
36+
try {
37+
byte[] hash = generateHash(key, counter);
38+
return getDigitsFromHash(hash);
39+
} catch (Exception e) {
40+
throw new CodeGenerationException("Failed to generate code. See nested exception.", e);
41+
}
42+
}
43+
44+
/**
45+
* Generate a HMAC-SHA1 hash of the counter number.
46+
*/
47+
private byte[] generateHash(String key, long counter) throws InvalidKeyException, NoSuchAlgorithmException {
48+
byte[] data = new byte[8];
49+
long value = counter;
50+
for (int i = 8; i-- > 0; value >>>= 8) {
51+
data[i] = (byte) value;
52+
}
53+
54+
// Create a HMAC-SHA1 signing key from the shared key
55+
Base32 codec = new Base32();
56+
byte[] decodedKey = codec.decode(key);
57+
SecretKeySpec signKey = new SecretKeySpec(decodedKey, algorithm.getHmacAlgorithm());
58+
Mac mac = Mac.getInstance(algorithm.getHmacAlgorithm());
59+
mac.init(signKey);
60+
61+
// Create a hash of the counter value
62+
return mac.doFinal(data);
63+
}
64+
65+
/**
66+
* Get the n-digit code for a given hash.
67+
*/
68+
private String getDigitsFromHash(byte[] hash) {
69+
int offset = hash[hash.length - 1] & 0xF;
70+
71+
long truncatedHash = 0;
72+
73+
for (int i = 0; i < 4; ++i) {
74+
truncatedHash <<= 8;
75+
truncatedHash |= (hash[offset + i] & 0xFF);
76+
}
77+
78+
truncatedHash &= 0x7FFFFFFF;
79+
truncatedHash %= Math.pow(10, digits);
80+
81+
// Left pad with 0s for a n-digit code
82+
return String.format("%0" + digits + "d", truncatedHash);
83+
}
84+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (c) 2019 Sam Stevens
3+
*
4+
* Licensed under the MIT License
5+
*
6+
* https://github.com/samdjstevens v1.7.1
7+
*/
8+
9+
package org.labkey.remoteapi.totp;
10+
11+
public enum HashingAlgorithm {
12+
13+
SHA1("HmacSHA1", "SHA1"), //default
14+
SHA256("HmacSHA256", "SHA256"),
15+
SHA512("HmacSHA512", "SHA512");
16+
17+
private final String hmacAlgorithm;
18+
private final String friendlyName;
19+
20+
HashingAlgorithm(String hmacAlgorithm, String friendlyName) {
21+
this.hmacAlgorithm = hmacAlgorithm;
22+
this.friendlyName = friendlyName;
23+
}
24+
25+
public String getHmacAlgorithm() {
26+
return hmacAlgorithm;
27+
}
28+
29+
public String getFriendlyName() {
30+
return friendlyName;
31+
}
32+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright (c) 2019 Sam Stevens
3+
*
4+
* Licensed under the MIT License
5+
*
6+
* https://github.com/samdjstevens v1.7.1
7+
*/
8+
package org.labkey.remoteapi.totp;
9+
10+
import java.time.Instant;
11+
12+
public class TimeProvider {
13+
public long getTime() throws RuntimeException {
14+
return Instant.now().getEpochSecond();
15+
}
16+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.labkey.remoteapi.totp;
2+
3+
public class TotpManager
4+
{
5+
// Generate a TOTP one-time password based on the default parameters (30 second time step, 6 digits, SHA1)
6+
public static String generateCode(String secretKey) throws CodeGenerationException
7+
{
8+
return generateCode(secretKey, 30, 6, HashingAlgorithm.SHA1);
9+
}
10+
11+
// Generate a TOTP one-time password using custom parameters
12+
public static String generateCode(String secretKey, int timeStep, int digits, HashingAlgorithm hashingAlgorithm) throws CodeGenerationException
13+
{
14+
TimeProvider timeProvider = new TimeProvider();
15+
long currentBucket = Math.floorDiv(timeProvider.getTime(), timeStep);
16+
CodeGenerator codeGenerator = new CodeGenerator(hashingAlgorithm, digits);
17+
18+
return codeGenerator.generate(secretKey, currentBucket);
19+
}
20+
}

0 commit comments

Comments
 (0)