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.content.res.Resources;
32 import android.net.Uri;
34 import com.google.android.apps.authenticator.Base32String;
35 import com.google.android.apps.authenticator.Base32String.DecodingException;
38 public static class TokenUriInvalidException extends Exception {
39 private static final long serialVersionUID = -1108624734612362345L;
40 private static int errorResourceID = 0;
41 public TokenUriInvalidException(int id) {
42 this.errorResourceID = id;
44 public int getErrorResourceID() {
45 return this.errorResourceID;
49 public static enum TokenType {
53 private final String mIssuerInt;
54 private final String mIssuerExt;
55 private final String mLabel;
56 private TokenType mType;
57 private String mAlgorithm;
58 private byte[] mSecret;
60 private long mCounter;
62 private long mLastCode;
64 private Token(Uri uri) throws TokenUriInvalidException {
65 String scheme = uri.getScheme();
66 String authority = uri.getAuthority();
67 String path = uri.getPath();
70 throw new TokenUriInvalidException(R.string.error_no_scheme);
71 if (authority == null)
72 throw new TokenUriInvalidException(R.string.error_no_authority);
74 throw new TokenUriInvalidException(R.string.error_no_path);
76 if (!scheme.equals("otpauth"))
77 throw new TokenUriInvalidException(R.string.error_invalid_scheme);
79 if (authority.equals("totp"))
80 mType = TokenType.TOTP;
81 else if (authority.equals("hotp"))
82 mType = TokenType.HOTP;
84 throw new TokenUriInvalidException(R.string.error_invalid_authority);
86 // Strip the path of its leading '/'
87 for (int i = 0; path.charAt(i) == '/'; i++)
88 path = path.substring(1);
89 if (path.length() == 0)
90 throw new TokenUriInvalidException(R.string.error_invalid_path);
92 int i = path.indexOf(':');
93 mIssuerExt = i < 0 ? "" : path.substring(0, i);
94 mIssuerInt = uri.getQueryParameter("issuer");
95 mLabel = path.substring(i >= 0 ? i + 1 : 0);
97 mAlgorithm = uri.getQueryParameter("algorithm");
98 if (mAlgorithm == null)
100 mAlgorithm = mAlgorithm.toUpperCase(Locale.US);
102 Mac.getInstance("Hmac" + mAlgorithm);
103 } catch (NoSuchAlgorithmException e1) {
104 throw new TokenUriInvalidException(R.string.error_no_algorithm);
108 String d = uri.getQueryParameter("digits");
111 mDigits = Integer.parseInt(d);
112 if (mDigits != 6 && mDigits != 8)
113 throw new TokenUriInvalidException(R.string.error_invalid_digits);
114 } catch (NumberFormatException e) {
115 throw new TokenUriInvalidException(R.string.error_invalid_number);
121 String c = uri.getQueryParameter("counter");
124 mCounter = Long.parseLong(c) - 1;
125 } catch (NumberFormatException e) {
126 throw new TokenUriInvalidException(R.string.error_invalid_counter);
131 String p = uri.getQueryParameter("period");
134 mPeriod = Integer.parseInt(p);
135 } catch (NumberFormatException e) {
136 throw new TokenUriInvalidException(R.string.error_invalid_period);
142 String s = uri.getQueryParameter("secret");
143 mSecret = Base32String.decode(s);
144 } catch (DecodingException e) {
145 throw new TokenUriInvalidException(R.string.error_invalid_secret);
149 private String getHOTP(long counter) {
150 // Encode counter in network byte order
151 ByteBuffer bb = ByteBuffer.allocate(8);
154 // Create digits divisor
156 for (int i = mDigits; i > 0; i--)
161 Mac mac = Mac.getInstance("Hmac" + mAlgorithm);
162 mac.init(new SecretKeySpec(mSecret, "Hmac" + mAlgorithm));
165 byte[] digest = mac.doFinal(bb.array());
169 int off = digest[digest.length - 1] & 0xf;
170 binary = (digest[off + 0] & 0x7f) << 0x18;
171 binary |= (digest[off + 1] & 0xff) << 0x10;
172 binary |= (digest[off + 2] & 0xff) << 0x08;
173 binary |= (digest[off + 3] & 0xff) << 0x00;
174 binary = binary % div;
177 String hotp = Integer.toString(binary);
178 while (hotp.length() != mDigits)
182 } catch (InvalidKeyException e) {
184 } catch (NoSuchAlgorithmException e) {
191 public Token(String uri) throws TokenUriInvalidException {
192 this(Uri.parse(uri));
195 public void increment() {
196 if (mType == TokenType.HOTP) {
198 mLastCode = System.currentTimeMillis();
202 public String getID() {
204 if (mIssuerInt != null && !mIssuerInt.equals(""))
205 id = mIssuerInt + ":" + mLabel;
206 else if (mIssuerExt != null && !mIssuerExt.equals(""))
207 id = mIssuerExt + ":" + mLabel;
214 public String getIssuer() {
215 return mIssuerExt != null ? mIssuerExt : "";
218 public String getLabel() {
219 return mLabel != null ? mLabel : "";
222 public String getCode() {
223 if (mType == TokenType.TOTP)
224 return getHOTP(System.currentTimeMillis() / 1000 / mPeriod);
226 long time = System.currentTimeMillis();
227 if (time - mLastCode > 60000) {
228 StringBuilder sb = new StringBuilder(mDigits);
229 for (int i = 0; i < mDigits; i++)
231 return sb.toString();
234 return getHOTP(mCounter);
237 public TokenType getType() {
241 // Progress is on a scale from 0 - 1000.
242 public int getProgress() {
243 long time = System.currentTimeMillis();
245 if (mType == TokenType.TOTP)
246 return 1000 - (int) (time % (mPeriod * 1000) / mPeriod);
248 long state = (time - mLastCode) / 60;
249 return 1000 - (int) (state > 1000 ? 1000 : state);
253 String issuerLabel = !mIssuerExt.equals("") ? mIssuerExt + ":" + mLabel : mLabel;
255 Uri.Builder builder = new Uri.Builder()
258 .appendQueryParameter("secret", Base32String.encode(mSecret))
259 .appendQueryParameter("issuer", mIssuerInt == null ? mIssuerExt : mIssuerInt)
260 .appendQueryParameter("algorithm", mAlgorithm)
261 .appendQueryParameter("digits", Integer.toString(mDigits));
265 builder.authority("hotp");
266 builder.appendQueryParameter("counter", Long.toString(mCounter + 1));
269 builder.authority("totp");
270 builder.appendQueryParameter("period", Integer.toString(mPeriod));
274 return builder.build();
278 public String toString() {
279 return toUri().toString();