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 issuerInt;
46 private final String issuerExt;
47 private final String label;
48 private TokenType type;
55 private Token(Uri uri) throws TokenUriInvalidException {
56 if (!uri.getScheme().equals("otpauth"))
57 throw new TokenUriInvalidException();
59 if (uri.getAuthority().equals("totp"))
60 type = TokenType.TOTP;
61 else if (uri.getAuthority().equals("hotp"))
62 type = TokenType.HOTP;
64 throw new TokenUriInvalidException();
66 String path = uri.getPath();
68 throw new TokenUriInvalidException();
70 // Strip the path of its leading '/'
71 for (int i = 0; path.charAt(i) == '/'; i++)
72 path = path.substring(1);
73 if (path.length() == 0)
74 throw new TokenUriInvalidException();
76 int i = path.indexOf(':');
77 issuerExt = i < 0 ? "" : path.substring(0, i);
78 issuerInt = uri.getQueryParameter("issuer");
79 label = path.substring(i >= 0 ? i + 1 : 0);
81 algo = uri.getQueryParameter("algorithm");
84 algo = algo.toUpperCase(Locale.US);
86 Mac.getInstance("Hmac" + algo);
87 } catch (NoSuchAlgorithmException e1) {
88 throw new TokenUriInvalidException();
92 String d = uri.getQueryParameter("digits");
95 digits = Integer.parseInt(d);
96 if (digits != 6 && digits != 8)
97 throw new TokenUriInvalidException();
98 } catch (NumberFormatException e) {
99 throw new TokenUriInvalidException();
105 String c = uri.getQueryParameter("counter");
108 counter = Long.parseLong(c);
109 } catch (NumberFormatException e) {
110 throw new TokenUriInvalidException();
115 String p = uri.getQueryParameter("period");
118 period = Integer.parseInt(p);
119 } catch (NumberFormatException e) {
120 throw new TokenUriInvalidException();
126 String s = uri.getQueryParameter("secret");
127 key = Base32String.decode(s);
128 } catch (DecodingException e) {
129 throw new TokenUriInvalidException();
133 private String getHOTP(long counter) {
134 // Encode counter in network byte order
135 ByteBuffer bb = ByteBuffer.allocate(8);
138 // Create digits divisor
140 for (int i = digits; i > 0; i--)
145 Mac mac = Mac.getInstance("Hmac" + algo);
146 mac.init(new SecretKeySpec(key, "Hmac" + algo));
149 byte[] digest = mac.doFinal(bb.array());
153 int off = digest[digest.length - 1] & 0xf;
154 binary = (digest[off + 0] & 0x7f) << 0x18;
155 binary |= (digest[off + 1] & 0xff) << 0x10;
156 binary |= (digest[off + 2] & 0xff) << 0x08;
157 binary |= (digest[off + 3] & 0xff) << 0x00;
158 binary = binary % div;
161 String hotp = Integer.toString(binary);
162 while (hotp.length() != digits)
166 } catch (InvalidKeyException e) {
168 } catch (NoSuchAlgorithmException e) {
175 public Token(String uri) throws TokenUriInvalidException {
176 this(Uri.parse(uri));
179 public String getID() {
181 if (issuerInt != null && !issuerInt.equals(""))
182 id = issuerInt + ":" + label;
183 else if (issuerExt != null && !issuerExt.equals(""))
184 id = issuerExt + ":" + label;
191 public String getIssuer() {
192 return issuerExt != null ? issuerExt : "";
195 public String getLabel() {
196 return label != null ? label : "";
199 public String getCode() {
202 return getHOTP(counter);
204 return getHOTP(System.currentTimeMillis() / 1000 / period);
210 public String getPlaceholder() {
211 StringBuilder sb = new StringBuilder(digits);
212 for (int i = 0; i < digits; i++)
214 return sb.toString();
217 public void increment() {
218 if (type == TokenType.HOTP)
223 String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label;
225 Uri.Builder builder = new Uri.Builder()
228 .appendQueryParameter("secret", Base32String.encode(key))
229 .appendQueryParameter("issuer", issuerInt == null ? issuerExt : issuerInt)
230 .appendQueryParameter("algorithm", algo)
231 .appendQueryParameter("digits", Integer.toString(digits));
235 builder.authority("hotp");
236 builder.appendQueryParameter("counter", Long.toString(counter));
239 builder.authority("totp");
240 builder.appendQueryParameter("period", Integer.toString(period));
244 return builder.build();
247 public TokenType getType() {
251 // Progress is on a scale from 0 - 1000.
252 public int getProgress() {
255 long time = System.currentTimeMillis() / 100;
256 return (int) ((time % p) * 1000 / p);
260 public String toString() {
261 return toUri().toString();