Java 代码之一次性密码算法

1. HOTP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package com.demo.sign.otp;

import java.nio.ByteBuffer;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class HOTP {

private static final int DEFAULT_PASSWORD_LENGTH = 6;
private static final String HOTP_HMAC_ALGORITHM = "HmacSHA1";

private final Mac prototypeMac;
private final int passwordLength;
private final int modDivisor;
private final String formatString;

public HOTP() throws NoSuchAlgorithmException {
this(DEFAULT_PASSWORD_LENGTH, HOTP_HMAC_ALGORITHM);
}

public HOTP(final String algorithm) throws NoSuchAlgorithmException {
this(DEFAULT_PASSWORD_LENGTH, algorithm);
}

public HOTP(final int passwordLength, final String algorithm) throws NoSuchAlgorithmException {
if (!PasswordLength.getValidPasswordLength().contains(passwordLength)) {
throw new IllegalArgumentException("Password length must be between 6 and 8 digits.");
}
// https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Mac
this.prototypeMac = Mac.getInstance(algorithm);
PasswordLength pl = PasswordLength.getByPasswordLength(passwordLength);
this.modDivisor = pl.getModDivisor();
this.formatString = pl.getFormatString();
this.passwordLength = passwordLength;
}

private enum PasswordLength {
SIX(6, 1_000_000, "%06d"),
SEVEN(7, 10_000_000, "%07d"),
EIGHT(8, 100_000_000, "%08d"),;

private int passwordLength;
private int modDivisor;
private String formatString;

private PasswordLength(int passwordLength, int modDivisor, String formatString) {
this.passwordLength = passwordLength;
this.modDivisor = modDivisor;
this.formatString = formatString;
}

public int getPasswordLength() {
return passwordLength;
}

public int getModDivisor() {
return modDivisor;
}

public String getFormatString() {
return formatString;
}

public static PasswordLength getByPasswordLength(int passwordLength) {
for (PasswordLength pl : values()) {
if (passwordLength == pl.getPasswordLength()) {
return pl;
}
}
return SIX;
}

public static List<Integer> getValidPasswordLength() {
return Stream.of(values()).map(PasswordLength::getPasswordLength).collect(Collectors.toList());
}
}

public int genHOTP(final String data, final long counter) throws Exception {
return genHOTP(new SecretKeySpec(data.getBytes(), "AES"), counter);
}

public int genHOTP(final Key key, final long counter) throws Exception {
final Mac mac = getMac();
final ByteBuffer buffer = ByteBuffer.allocate(mac.getMacLength());
buffer.putLong(0, counter);

final byte[] array = buffer.array();
mac.init(key);
mac.update(array, 0, 8);
mac.doFinal(array, 0);

final int offset = buffer.get(buffer.capacity() - 1) & 0x0f;
return (buffer.getInt(offset) & 0x7fffffff) % this.modDivisor;
}

private Mac getMac() {
try {
// Cloning is generally cheaper than `Mac.getInstance`.
return (Mac)this.prototypeMac.clone();
} catch (CloneNotSupportedException e) {
try {
return Mac.getInstance(this.prototypeMac.getAlgorithm());
} catch (final NoSuchAlgorithmException ex) {
throw new RuntimeException(ex);
}
}
}

public String genHOTPString(final String data, final long counter) throws Exception {
return this.genHOTPString(data, counter, Locale.getDefault());
}

public String genHOTPString(final String data, final long counter, final Locale locale) throws Exception {
return this.formatOTP(genHOTP(data, counter), locale);
}

public String genHOTPString(final Key key, final long counter) throws Exception {
return this.genHOTPString(key, counter, Locale.getDefault());
}

public String genHOTPString(final Key key, final long counter, final Locale locale) throws Exception {
return this.formatOTP(genHOTP(key, counter), locale);
}

private String formatOTP(final int oneTimePassword, final Locale locale) {
return String.format(locale, formatString, oneTimePassword);
}

public int getPasswordLength() {
return this.passwordLength;
}

public String getAlgorithm() {
return this.prototypeMac.getAlgorithm();
}
}