-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPasscodeGenerator.java
More file actions
244 lines (219 loc) · 7.37 KB
/
PasscodeGenerator.java
File metadata and controls
244 lines (219 loc) · 7.37 KB
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/*-
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.authenticator.blackberry;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import org.bouncycastle.crypto.Mac;
/**
* An implementation of the HOTP generator specified by RFC 4226. Generates
* short passcodes that may be used in challenge-response protocols or as
* timeout passcodes that are only valid for a short period.
*
* The default passcode is a 6-digit decimal code and the default timeout
* period is 5 minutes.
*/
public class PasscodeGenerator {
/** Default decimal passcode length */
private static final int PASS_CODE_LENGTH = 6;
/** Default passcode timeout period (in seconds) */
private static final int INTERVAL = 30;
/** The number of previous and future intervals to check */
private static final int ADJACENT_INTERVALS = 1;
private static final int PIN_MODULO = pow(10, PASS_CODE_LENGTH);
private static final int pow(int a, int b) {
int result = 1;
for (int i = 0; i < b; i++) {
result *= a;
}
return result;
}
private final Signer signer;
private final int codeLength;
private final int intervalPeriod;
/*
* Using an interface to allow us to inject different signature
* implementations.
*/
interface Signer {
byte[] sign(byte[] data);
}
/**
* @param mac A {@link Mac} used to generate passcodes
*/
public PasscodeGenerator(Mac mac) {
this(mac, PASS_CODE_LENGTH, INTERVAL);
}
/**
* @param mac A {@link Mac} used to generate passcodes
* @param passCodeLength The length of the decimal passcode
* @param interval The interval that a passcode is valid for
*/
public PasscodeGenerator(final Mac mac, int passCodeLength, int interval) {
this(new Signer() {
public byte[] sign(byte[] data){
mac.reset();
mac.update(data, 0, data.length);
int length = mac.getMacSize();
byte[] out = new byte[length];
mac.doFinal(out, 0);
mac.reset();
return out;
}
}, passCodeLength, interval);
}
public PasscodeGenerator(Signer signer, int passCodeLength, int interval) {
this.signer = signer;
this.codeLength = passCodeLength;
this.intervalPeriod = interval;
}
private String padOutput(int value) {
String result = Integer.toString(value);
for (int i = result.length(); i < codeLength; i++) {
result = "0" + result;
}
return result;
}
/**
* @return A decimal timeout code
*/
public String generateTimeoutCode() {
return generateResponseCode(clock.getCurrentInterval());
}
/**
* @param challenge A long-valued challenge
* @return A decimal response code
* @throws GeneralSecurityException If a JCE exception occur
*/
public String generateResponseCode(long challenge) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutput dout = new DataOutputStream(out);
try {
dout.writeLong(challenge);
} catch (IOException e) {
// This should never happen with a ByteArrayOutputStream
throw new RuntimeException("Unexpected IOException");
}
byte[] value = out.toByteArray();
return generateResponseCode(value);
}
/**
* @param challenge An arbitrary byte array used as a challenge
* @return A decimal response code
* @throws GeneralSecurityException If a JCE exception occur
*/
public String generateResponseCode(byte[] challenge) {
byte[] hash = signer.sign(challenge);
// Dynamically truncate the hash
// OffsetBits are the low order bits of the last byte of the hash
int offset = hash[hash.length - 1] & 0xF;
// Grab a positive integer value starting at the given offset.
int truncatedHash = hashToInt(hash, offset) & 0x7FFFFFFF;
int pinValue = truncatedHash % PIN_MODULO;
return padOutput(pinValue);
}
/**
* Grabs a positive integer value from the input array starting at
* the given offset.
* @param bytes the array of bytes
* @param start the index into the array to start grabbing bytes
* @return the integer constructed from the four bytes in the array
*/
private int hashToInt(byte[] bytes, int start) {
DataInput input = new DataInputStream(
new ByteArrayInputStream(bytes, start, bytes.length - start));
int val;
try {
val = input.readInt();
} catch (IOException e) {
throw new IllegalStateException(String.valueOf(e));
}
return val;
}
/**
* @param challenge A challenge to check a response against
* @param response A response to verify
* @return True if the response is valid
*/
public boolean verifyResponseCode(long challenge, String response) {
String expectedResponse = generateResponseCode(challenge);
return expectedResponse.equals(response);
}
/**
* Verify a timeout code. The timeout code will be valid for a time
* determined by the interval period and the number of adjacent intervals
* checked.
*
* @param timeoutCode The timeout code
* @return True if the timeout code is valid
*/
public boolean verifyTimeoutCode(String timeoutCode) {
return verifyTimeoutCode(timeoutCode, ADJACENT_INTERVALS,
ADJACENT_INTERVALS);
}
/**
* Verify a timeout code. The timeout code will be valid for a time
* determined by the interval period and the number of adjacent intervals
* checked.
*
* @param timeoutCode The timeout code
* @param pastIntervals The number of past intervals to check
* @param futureIntervals The number of future intervals to check
* @return True if the timeout code is valid
*/
public boolean verifyTimeoutCode(String timeoutCode, int pastIntervals,
int futureIntervals) {
long currentInterval = clock.getCurrentInterval();
String expectedResponse = generateResponseCode(currentInterval);
if (expectedResponse.equals(timeoutCode)) {
return true;
}
for (int i = 1; i <= pastIntervals; i++) {
String pastResponse = generateResponseCode(currentInterval - i);
if (pastResponse.equals(timeoutCode)) {
return true;
}
}
for (int i = 1; i <= futureIntervals; i++) {
String futureResponse = generateResponseCode(currentInterval + i);
if (futureResponse.equals(timeoutCode)) {
return true;
}
}
return false;
}
private IntervalClock clock = new IntervalClock() {
/*
* @return The current interval
*/
public long getCurrentInterval() {
long currentTimeSeconds = System.currentTimeMillis() / 1000;
return currentTimeSeconds / getIntervalPeriod();
}
public int getIntervalPeriod() {
return intervalPeriod;
}
};
// To facilitate injecting a mock clock
interface IntervalClock {
int getIntervalPeriod();
long getCurrentInterval();
}
}