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.ArrayList;
27 import java.util.List;
28 import java.util.Locale;
30 import javax.crypto.Mac;
31 import javax.crypto.spec.SecretKeySpec;
33 import android.content.Context;
34 import android.content.SharedPreferences;
35 import android.net.Uri;
37 import com.google.android.apps.authenticator.Base32String;
38 import com.google.android.apps.authenticator.Base32String.DecodingException;
41 public static class TokenUriInvalidException extends Exception {
42 private static final long serialVersionUID = -1108624734612362345L;
45 public static enum TokenType {
49 private final String issuerInt;
50 private final String issuerExt;
51 private final String label;
52 private TokenType type;
59 public static List<Token> getTokens(Context ctx) {
60 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
62 List<Token> tokens = new ArrayList<Token>();
63 for (String key : prefs.getAll().keySet()) {
65 tokens.add(new Token(prefs.getString(key, null)));
66 } catch (TokenUriInvalidException e) {
74 private Token(Uri uri) throws TokenUriInvalidException {
75 if (!uri.getScheme().equals("otpauth"))
76 throw new TokenUriInvalidException();
78 if (uri.getAuthority().equals("totp"))
79 type = TokenType.TOTP;
80 else if (uri.getAuthority().equals("hotp"))
81 type = TokenType.HOTP;
83 throw new TokenUriInvalidException();
85 String path = uri.getPath();
87 throw new TokenUriInvalidException();
89 // Strip the path of its leading '/'
90 for (int i = 0; path.charAt(i) == '/'; i++)
91 path = path.substring(1);
92 if (path.length() == 0)
93 throw new TokenUriInvalidException();
95 int i = path.indexOf(':');
96 issuerExt = i < 0 ? "" : path.substring(0, i);
97 issuerInt = uri.getQueryParameter("issuer");
98 label = path.substring(i >= 0 ? i + 1 : 0);
100 algo = uri.getQueryParameter("algorithm");
103 algo = algo.toUpperCase(Locale.US);
105 Mac.getInstance("Hmac" + algo);
106 } catch (NoSuchAlgorithmException e1) {
107 throw new TokenUriInvalidException();
111 String d = uri.getQueryParameter("digits");
114 digits = Integer.parseInt(d);
115 if (digits != 6 && digits != 8)
116 throw new TokenUriInvalidException();
117 } catch (NumberFormatException e) {
118 throw new TokenUriInvalidException();
124 String c = uri.getQueryParameter("counter");
127 counter = Long.parseLong(c);
128 } catch (NumberFormatException e) {
129 throw new TokenUriInvalidException();
134 String p = uri.getQueryParameter("period");
137 period = Integer.parseInt(p);
138 } catch (NumberFormatException e) {
139 throw new TokenUriInvalidException();
145 String s = uri.getQueryParameter("secret");
146 key = Base32String.decode(s);
147 } catch (DecodingException e) {
148 throw new TokenUriInvalidException();
152 private String getHOTP(long counter) {
153 // Encode counter in network byte order
154 ByteBuffer bb = ByteBuffer.allocate(8);
157 // Create digits divisor
159 for (int i = digits; i > 0; i--)
164 Mac mac = Mac.getInstance("Hmac" + algo);
165 mac.init(new SecretKeySpec(key, "Hmac" + algo));
168 byte[] digest = mac.doFinal(bb.array());
172 int off = digest[digest.length - 1] & 0xf;
173 binary = (digest[off + 0] & 0x7f) << 0x18;
174 binary |= (digest[off + 1] & 0xff) << 0x10;
175 binary |= (digest[off + 2] & 0xff) << 0x08;
176 binary |= (digest[off + 3] & 0xff) << 0x00;
177 binary = binary % div;
180 String hotp = Integer.toString(binary);
181 while (hotp.length() != digits)
185 } catch (InvalidKeyException e) {
187 } catch (NoSuchAlgorithmException e) {
194 public Token(String uri) throws TokenUriInvalidException {
195 this(Uri.parse(uri));
198 private String getId() {
200 if (issuerInt != null && !issuerInt.equals(""))
201 id = issuerInt + ":" + label;
202 else if (issuerExt != null && !issuerExt.equals(""))
203 id = issuerExt + ":" + label;
210 public void remove(Context ctx) {
211 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
212 prefs.edit().remove(getId()).apply();
215 public void save(Context ctx) {
216 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
217 prefs.edit().putString(getId(), toString()).apply();
220 public String getTitle() {
222 if (issuerExt != null && !issuerExt.equals(""))
223 title += issuerExt + ": ";
228 public String getCurrentTokenValue(Context ctx, boolean increment) {
229 if (type == TokenType.HOTP) {
232 return getHOTP(counter++);
237 String placeholder = "";
238 for (int i = 0; i < digits; i++)
245 return getHOTP(System.currentTimeMillis() / 1000 / period);
249 String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label;
251 Uri.Builder builder = new Uri.Builder()
254 .appendQueryParameter("secret", Base32String.encode(key))
255 .appendQueryParameter("issuer", issuerInt == null ? issuerExt : issuerInt)
256 .appendQueryParameter("algorithm", algo)
257 .appendQueryParameter("digits", Integer.toString(digits));
261 builder.authority("hotp");
262 builder.appendQueryParameter("counter", Long.toString(counter));
265 builder.authority("totp");
266 builder.appendQueryParameter("period", Integer.toString(period));
270 return builder.build();
273 public TokenType getType() {
277 // Progress is on a scale from 0 - 1000.
278 public int getProgress() {
281 long time = System.currentTimeMillis() / 100;
282 return (int) ((time % p) * 1000 / p);
286 public String toString() {
287 return toUri().toString();