4 * Authors: Nathaniel McCallum <npmccallum@redhat.com>
6 * Copyright (C) 2013 Nathaniel McCallum, Red Hat
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
21 package org.fedorahosted.freeotp;
23 import java.nio.ByteBuffer;
24 import java.security.InvalidKeyException;
25 import java.security.NoSuchAlgorithmException;
26 import java.util.Locale;
28 import javax.crypto.Mac;
29 import javax.crypto.spec.SecretKeySpec;
31 import android.net.Uri;
33 import com.google.android.apps.authenticator.Base32String;
34 import com.google.android.apps.authenticator.Base32String.DecodingException;
37 public static class TokenUriInvalidException extends Exception {
38 private static final long serialVersionUID = -1108624734612362345L;
41 public static enum TokenType {
45 private final String mIssuerInt;
46 private final String mIssuerExt;
47 private final String mLabel;
48 private TokenType mType;
49 private String mAlgorithm;
50 private byte[] mSecret;
52 private long mCounter;
54 private long mLastCode;
56 private Token(Uri uri) throws TokenUriInvalidException {
57 String scheme = uri.getScheme();
58 String authority = uri.getAuthority();
59 String path = uri.getPath();
61 if (scheme == null || authority == null || path == null)
62 throw new TokenUriInvalidException();
64 if (!scheme.equals("otpauth"))
65 throw new TokenUriInvalidException();
67 if (authority.equals("totp"))
68 mType = TokenType.TOTP;
69 else if (authority.equals("hotp"))
70 mType = TokenType.HOTP;
72 throw new TokenUriInvalidException();
74 // Strip the path of its leading '/'
75 for (int i = 0; path.charAt(i) == '/'; i++)
76 path = path.substring(1);
77 if (path.length() == 0)
78 throw new TokenUriInvalidException();
80 int i = path.indexOf(':');
81 mIssuerExt = i < 0 ? "" : path.substring(0, i);
82 mIssuerInt = uri.getQueryParameter("issuer");
83 mLabel = path.substring(i >= 0 ? i + 1 : 0);
85 mAlgorithm = uri.getQueryParameter("algorithm");
86 if (mAlgorithm == null)
88 mAlgorithm = mAlgorithm.toUpperCase(Locale.US);
90 Mac.getInstance("Hmac" + mAlgorithm);
91 } catch (NoSuchAlgorithmException e1) {
92 throw new TokenUriInvalidException();
96 String d = uri.getQueryParameter("digits");
99 mDigits = Integer.parseInt(d);
100 if (mDigits != 6 && mDigits != 8)
101 throw new TokenUriInvalidException();
102 } catch (NumberFormatException e) {
103 throw new TokenUriInvalidException();
109 String c = uri.getQueryParameter("counter");
112 mCounter = Long.parseLong(c) - 1;
113 } catch (NumberFormatException e) {
114 throw new TokenUriInvalidException();
119 String p = uri.getQueryParameter("period");
122 mPeriod = Integer.parseInt(p);
123 } catch (NumberFormatException e) {
124 throw new TokenUriInvalidException();
130 String s = uri.getQueryParameter("secret");
131 mSecret = Base32String.decode(s);
132 } catch (DecodingException e) {
133 throw new TokenUriInvalidException();
137 private String getHOTP(long counter) {
138 // Encode counter in network byte order
139 ByteBuffer bb = ByteBuffer.allocate(8);
142 // Create digits divisor
144 for (int i = mDigits; i > 0; i--)
149 Mac mac = Mac.getInstance("Hmac" + mAlgorithm);
150 mac.init(new SecretKeySpec(mSecret, "Hmac" + mAlgorithm));
153 byte[] digest = mac.doFinal(bb.array());
157 int off = digest[digest.length - 1] & 0xf;
158 binary = (digest[off + 0] & 0x7f) << 0x18;
159 binary |= (digest[off + 1] & 0xff) << 0x10;
160 binary |= (digest[off + 2] & 0xff) << 0x08;
161 binary |= (digest[off + 3] & 0xff) << 0x00;
162 binary = binary % div;
165 String hotp = Integer.toString(binary);
166 while (hotp.length() != mDigits)
170 } catch (InvalidKeyException e) {
172 } catch (NoSuchAlgorithmException e) {
179 public Token(String uri) throws TokenUriInvalidException {
180 this(Uri.parse(uri));
183 public void increment() {
184 if (mType == TokenType.HOTP) {
186 mLastCode = System.currentTimeMillis();
190 public String getID() {
192 if (mIssuerInt != null && !mIssuerInt.equals(""))
193 id = mIssuerInt + ":" + mLabel;
194 else if (mIssuerExt != null && !mIssuerExt.equals(""))
195 id = mIssuerExt + ":" + mLabel;
202 public String getIssuer() {
203 return mIssuerExt != null ? mIssuerExt : "";
206 public String getLabel() {
207 return mLabel != null ? mLabel : "";
210 public String getCode() {
211 if (mType == TokenType.TOTP)
212 return getHOTP(System.currentTimeMillis() / 1000 / mPeriod);
214 long time = System.currentTimeMillis();
215 if (time - mLastCode > 60000) {
216 StringBuilder sb = new StringBuilder(mDigits);
217 for (int i = 0; i < mDigits; i++)
219 return sb.toString();
222 return getHOTP(mCounter);
225 public TokenType getType() {
229 // Progress is on a scale from 0 - 1000.
230 public int getProgress() {
231 long time = System.currentTimeMillis();
233 if (mType == TokenType.TOTP)
234 return 1000 - (int) (time % (mPeriod * 1000) / mPeriod);
236 long state = (time - mLastCode) / 60;
237 return 1000 - (int) (state > 1000 ? 1000 : state);
241 String issuerLabel = !mIssuerExt.equals("") ? mIssuerExt + ":" + mLabel : mLabel;
243 Uri.Builder builder = new Uri.Builder()
246 .appendQueryParameter("secret", Base32String.encode(mSecret))
247 .appendQueryParameter("issuer", mIssuerInt == null ? mIssuerExt : mIssuerInt)
248 .appendQueryParameter("algorithm", mAlgorithm)
249 .appendQueryParameter("digits", Integer.toString(mDigits));
253 builder.authority("hotp");
254 builder.appendQueryParameter("counter", Long.toString(mCounter + 1));
257 builder.authority("totp");
258 builder.appendQueryParameter("period", Integer.toString(mPeriod));
262 return builder.build();
266 public String toString() {
267 return toUri().toString();