4 * Authors: Nathaniel McCallum <npmccallum@redhat.com>
6 * Copyright (C) 2013 Nathaniel McCallum, Red Hat
7 * see file 'COPYING' for use and warranty information
9 * This program is free software you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
23 package org.fedorahosted.freeotp;
25 import java.nio.ByteBuffer;
26 import java.security.InvalidKeyException;
27 import java.security.NoSuchAlgorithmException;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Locale;
32 import javax.crypto.Mac;
33 import javax.crypto.spec.SecretKeySpec;
35 import com.google.android.apps.authenticator.Base32String;
36 import com.google.android.apps.authenticator.Base32String.DecodingException;
38 import android.content.Context;
39 import android.content.SharedPreferences;
40 import android.net.Uri;
43 public static class TokenUriInvalidException extends Exception {
44 private static final long serialVersionUID = -1108624734612362345L;
47 public static enum TokenType {
51 private String issuerInt;
52 private String issuerExt;
54 private TokenType type;
61 public static List<Token> getTokens(Context ctx) {
62 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
64 List<Token> tokens = new ArrayList<Token>();
65 for (String key : prefs.getAll().keySet()) {
67 tokens.add(new Token(prefs.getString(key, null)));
68 } catch (TokenUriInvalidException e) {
70 } catch (NoSuchAlgorithmException e) {
78 private Token(Uri uri) throws TokenUriInvalidException, NoSuchAlgorithmException {
79 if (!uri.getScheme().equals("otpauth"))
80 throw new TokenUriInvalidException();
82 if (uri.getAuthority().equals("totp"))
83 type = TokenType.TOTP;
84 else if (uri.getAuthority().equals("hotp"))
85 type = TokenType.HOTP;
87 throw new TokenUriInvalidException();
89 String path = uri.getPath();
91 throw new TokenUriInvalidException();
93 // Strip the path of its leading '/'
94 for (int i = 0; path.charAt(i) == '/'; i++)
95 path = path.substring(1);
96 if (path.length() == 0)
97 throw new TokenUriInvalidException();
99 int i = path.indexOf(':');
100 issuerExt = i < 0 ? "" : path.substring(0, i);
101 issuerInt = uri.getQueryParameter("issuer");
102 label = path.substring(i >= 0 ? i + 1 : 0);
104 algo = uri.getQueryParameter("algorithm");
107 algo = algo.toUpperCase(Locale.US);
108 if (!algo.equals("SHA1") && !algo.equals("SHA256") &&
109 !algo.equals("SHA512") && !algo.equals("MD5"))
110 throw new TokenUriInvalidException();
111 Mac.getInstance("Hmac" + algo);
114 String d = uri.getQueryParameter("digits");
117 digits = Integer.parseInt(d);
118 if (digits != 6 && digits != 8)
119 throw new TokenUriInvalidException();
120 } catch (NumberFormatException e) {
121 throw new TokenUriInvalidException();
127 String c = uri.getQueryParameter("counter");
130 counter = Long.parseLong(c);
131 } catch (NumberFormatException e) {
132 throw new TokenUriInvalidException();
137 String p = uri.getQueryParameter("period");
140 period = Integer.parseInt(p);
141 } catch (NumberFormatException e) {
142 throw new TokenUriInvalidException();
148 String s = uri.getQueryParameter("secret");
149 key = Base32String.decode(s);
150 } catch (DecodingException e) {
151 throw new TokenUriInvalidException();
155 private String getHOTP(long counter) {
156 // Encode counter in network byte order
157 ByteBuffer bb = ByteBuffer.allocate(8);
160 // Create digits divisor
162 for (int i = digits; i > 0; i--)
167 Mac mac = Mac.getInstance("Hmac" + algo);
168 mac.init(new SecretKeySpec(key, "Hmac" + algo));
171 byte[] digest = mac.doFinal(bb.array());
175 int off = digest[digest.length - 1] & 0xf;
176 binary = (digest[off + 0] & 0x7f) << 0x18;
177 binary |= (digest[off + 1] & 0xff) << 0x10;
178 binary |= (digest[off + 2] & 0xff) << 0x08;
179 binary |= (digest[off + 3] & 0xff) << 0x00;
180 binary = binary % div;
183 String hotp = Integer.toString(binary);
184 while (hotp.length() != digits)
188 } catch (InvalidKeyException e) {
190 } catch (NoSuchAlgorithmException e) {
197 public Token(String uri) throws TokenUriInvalidException, NoSuchAlgorithmException {
198 this(Uri.parse(uri));
201 private String getId() {
203 if (issuerInt != null && !issuerInt.equals(""))
204 id = issuerInt + ":" + label;
205 else if (issuerExt != null && !issuerExt.equals(""))
206 id = issuerExt + ":" + label;
213 public void remove(Context ctx) {
214 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
215 prefs.edit().remove(getId()).apply();
218 public void save(Context ctx) {
219 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
220 prefs.edit().putString(getId(), toString()).apply();
223 public String getTitle() {
225 if (issuerExt != null && !issuerExt.equals(""))
226 title += issuerExt + ": ";
231 public String getCurrentTokenValue(Context ctx, boolean increment) {
232 if (type == TokenType.HOTP) {
235 return getHOTP(counter++);
240 String placeholder = "";
241 for (int i = 0; i < digits; i++)
248 return getHOTP(System.currentTimeMillis() / 1000 / period);
252 String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label;
254 Uri.Builder builder = new Uri.Builder()
257 .appendQueryParameter("secret", Base32String.encode(key))
258 .appendQueryParameter("issuer", issuerInt == null ? issuerExt : issuerInt)
259 .appendQueryParameter("algorithm", algo)
260 .appendQueryParameter("digits", Integer.toString(digits));
264 builder.authority("hotp");
265 builder.appendQueryParameter("counter", Long.toString(counter));
268 builder.authority("totp");
269 builder.appendQueryParameter("period", Integer.toString(period));
273 return builder.build();
276 public TokenType getType() {
280 // Progress is on a scale from 0 - 1000.
281 public int getProgress() {
284 long time = System.currentTimeMillis() / 100;
285 return (int) ((time % p) * 1000 / p);
289 public String toString() {
290 return toUri().toString();