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 if (!uri.getScheme().equals("otpauth"))
58 throw new TokenUriInvalidException();
60 if (uri.getAuthority().equals("totp"))
61 mType = TokenType.TOTP;
62 else if (uri.getAuthority().equals("hotp"))
63 mType = TokenType.HOTP;
65 throw new TokenUriInvalidException();
67 String path = uri.getPath();
69 throw new TokenUriInvalidException();
71 // Strip the path of its leading '/'
72 for (int i = 0; path.charAt(i) == '/'; i++)
73 path = path.substring(1);
74 if (path.length() == 0)
75 throw new TokenUriInvalidException();
77 int i = path.indexOf(':');
78 mIssuerExt = i < 0 ? "" : path.substring(0, i);
79 mIssuerInt = uri.getQueryParameter("issuer");
80 mLabel = path.substring(i >= 0 ? i + 1 : 0);
82 mAlgorithm = uri.getQueryParameter("algorithm");
83 if (mAlgorithm == null)
85 mAlgorithm = mAlgorithm.toUpperCase(Locale.US);
87 Mac.getInstance("Hmac" + mAlgorithm);
88 } catch (NoSuchAlgorithmException e1) {
89 throw new TokenUriInvalidException();
93 String d = uri.getQueryParameter("digits");
96 mDigits = Integer.parseInt(d);
97 if (mDigits != 6 && mDigits != 8)
98 throw new TokenUriInvalidException();
99 } catch (NumberFormatException e) {
100 throw new TokenUriInvalidException();
106 String c = uri.getQueryParameter("counter");
109 mCounter = Long.parseLong(c) - 1;
110 } catch (NumberFormatException e) {
111 throw new TokenUriInvalidException();
116 String p = uri.getQueryParameter("period");
119 mPeriod = Integer.parseInt(p);
120 } catch (NumberFormatException e) {
121 throw new TokenUriInvalidException();
127 String s = uri.getQueryParameter("secret");
128 mSecret = Base32String.decode(s);
129 } catch (DecodingException e) {
130 throw new TokenUriInvalidException();
134 private String getHOTP(long counter) {
135 // Encode counter in network byte order
136 ByteBuffer bb = ByteBuffer.allocate(8);
139 // Create digits divisor
141 for (int i = mDigits; i > 0; i--)
146 Mac mac = Mac.getInstance("Hmac" + mAlgorithm);
147 mac.init(new SecretKeySpec(mSecret, "Hmac" + mAlgorithm));
150 byte[] digest = mac.doFinal(bb.array());
154 int off = digest[digest.length - 1] & 0xf;
155 binary = (digest[off + 0] & 0x7f) << 0x18;
156 binary |= (digest[off + 1] & 0xff) << 0x10;
157 binary |= (digest[off + 2] & 0xff) << 0x08;
158 binary |= (digest[off + 3] & 0xff) << 0x00;
159 binary = binary % div;
162 String hotp = Integer.toString(binary);
163 while (hotp.length() != mDigits)
167 } catch (InvalidKeyException e) {
169 } catch (NoSuchAlgorithmException e) {
176 public Token(String uri) throws TokenUriInvalidException {
177 this(Uri.parse(uri));
180 public void increment() {
181 if (mType == TokenType.HOTP) {
183 mLastCode = System.currentTimeMillis();
187 public String getID() {
189 if (mIssuerInt != null && !mIssuerInt.equals(""))
190 id = mIssuerInt + ":" + mLabel;
191 else if (mIssuerExt != null && !mIssuerExt.equals(""))
192 id = mIssuerExt + ":" + mLabel;
199 public String getIssuer() {
200 return mIssuerExt != null ? mIssuerExt : "";
203 public String getLabel() {
204 return mLabel != null ? mLabel : "";
207 public String getCode() {
208 if (mType == TokenType.TOTP)
209 return getHOTP(System.currentTimeMillis() / 1000 / mPeriod);
211 long time = System.currentTimeMillis();
212 if (time - mLastCode > 60000) {
213 StringBuilder sb = new StringBuilder(mDigits);
214 for (int i = 0; i < mDigits; i++)
216 return sb.toString();
219 return getHOTP(mCounter);
222 public TokenType getType() {
226 // Progress is on a scale from 0 - 1000.
227 public int getProgress() {
228 long time = System.currentTimeMillis();
230 if (mType == TokenType.TOTP)
231 return (int) (time % (mPeriod * 1000) / mPeriod);
233 long state = (time - mLastCode) / 60;
234 return (int) (state > 1000 ? 1000 : state);
238 String issuerLabel = !mIssuerExt.equals("") ? mIssuerExt + ":" + mLabel : mLabel;
240 Uri.Builder builder = new Uri.Builder()
243 .appendQueryParameter("secret", Base32String.encode(mSecret))
244 .appendQueryParameter("issuer", mIssuerInt == null ? mIssuerExt : mIssuerInt)
245 .appendQueryParameter("algorithm", mAlgorithm)
246 .appendQueryParameter("digits", Integer.toString(mDigits));
250 builder.authority("hotp");
251 builder.appendQueryParameter("counter", Long.toString(mCounter + 1));
254 builder.authority("totp");
255 builder.appendQueryParameter("period", Integer.toString(mPeriod));
259 return builder.build();
263 public String toString() {
264 return toUri().toString();