]> Pileus Git - ~andy/freeotp/blob - src/org/fedorahosted/freeotp/Token.java
Clean up some formatting issues
[~andy/freeotp] / src / org / fedorahosted / freeotp / Token.java
1 /*
2  * FreeOTP
3  *
4  * Authors: Nathaniel McCallum <npmccallum@redhat.com>
5  *
6  * Copyright (C) 2013  Nathaniel McCallum, Red Hat
7  * see file 'COPYING' for use and warranty information
8  *
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.
13  *
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.
18  *
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/>.
21  */
22
23 package org.fedorahosted.freeotp;
24
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;
31
32 import javax.crypto.Mac;
33 import javax.crypto.spec.SecretKeySpec;
34
35 import android.content.Context;
36 import android.content.SharedPreferences;
37 import android.net.Uri;
38
39 import com.google.android.apps.authenticator.Base32String;
40 import com.google.android.apps.authenticator.Base32String.DecodingException;
41
42 public class Token {
43         public static class TokenUriInvalidException extends Exception {
44                 private static final long serialVersionUID = -1108624734612362345L;
45         }
46
47         public static enum TokenType {
48                 HOTP, TOTP
49         }
50
51         private final String issuerInt;
52         private final String issuerExt;
53         private final String label;
54         private TokenType type;
55         private String algo;
56         private byte[] key;
57         private int digits;
58         private long counter;
59         private int period;
60
61         public static List<Token> getTokens(Context ctx) {
62                 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
63
64                 List<Token> tokens = new ArrayList<Token>();
65                 for (String key : prefs.getAll().keySet()) {
66                         try {
67                                 tokens.add(new Token(prefs.getString(key, null)));
68                         } catch (TokenUriInvalidException e) {
69                                 e.printStackTrace();
70                         } catch (NoSuchAlgorithmException e) {
71                                 e.printStackTrace();
72                         }
73                 }
74
75                 return tokens;
76         }
77
78         private Token(Uri uri) throws TokenUriInvalidException, NoSuchAlgorithmException {
79                 if (!uri.getScheme().equals("otpauth"))
80                         throw new TokenUriInvalidException();
81
82                 if (uri.getAuthority().equals("totp"))
83                         type = TokenType.TOTP;
84                 else if (uri.getAuthority().equals("hotp"))
85                         type = TokenType.HOTP;
86                 else
87                         throw new TokenUriInvalidException();
88
89                 String path = uri.getPath();
90                 if (path == null)
91                         throw new TokenUriInvalidException();
92
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();
98
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);
103
104                 algo = uri.getQueryParameter("algorithm");
105                 if (algo == null)
106                         algo = "sha1";
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);
112
113                 try {
114                         String d = uri.getQueryParameter("digits");
115                         if (d == null)
116                                 d = "6";
117                         digits = Integer.parseInt(d);
118                         if (digits != 6 && digits != 8)
119                                 throw new TokenUriInvalidException();
120                 } catch (NumberFormatException e) {
121                         throw new TokenUriInvalidException();
122                 }
123
124                 switch (type) {
125                 case HOTP:
126                         try {
127                                 String c = uri.getQueryParameter("counter");
128                                 if (c == null)
129                                         c = "0";
130                                 counter = Long.parseLong(c);
131                         } catch (NumberFormatException e) {
132                                 throw new TokenUriInvalidException();
133                         }
134                         break;
135                 case TOTP:
136                         try {
137                                 String p = uri.getQueryParameter("period");
138                                 if (p == null)
139                                         p = "30";
140                                 period = Integer.parseInt(p);
141                         } catch (NumberFormatException e) {
142                                 throw new TokenUriInvalidException();
143                         }
144                         break;
145                 }
146
147                 try {
148                         String s = uri.getQueryParameter("secret");
149                         key = Base32String.decode(s);
150                 } catch (DecodingException e) {
151                         throw new TokenUriInvalidException();
152                 }
153         }
154
155         private String getHOTP(long counter) {
156                 // Encode counter in network byte order
157                 ByteBuffer bb = ByteBuffer.allocate(8);
158                 bb.putLong(counter);
159
160                 // Create digits divisor
161                 int div = 1;
162                 for (int i = digits; i > 0; i--)
163                         div *= 10;
164
165                 // Create the HMAC
166                 try {
167                         Mac mac = Mac.getInstance("Hmac" + algo);
168                         mac.init(new SecretKeySpec(key, "Hmac" + algo));
169
170                         // Do the hashing
171                         byte[] digest = mac.doFinal(bb.array());
172
173                         // Truncate
174                         int binary;
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;
181
182                         // Zero pad
183                         String hotp = Integer.toString(binary);
184                         while (hotp.length() != digits)
185                                 hotp = "0" + hotp;
186
187                         return hotp;
188                 } catch (InvalidKeyException e) {
189                         e.printStackTrace();
190                 } catch (NoSuchAlgorithmException e) {
191                         e.printStackTrace();
192                 }
193
194                 return "";
195         }
196
197         public Token(String uri) throws TokenUriInvalidException, NoSuchAlgorithmException {
198                 this(Uri.parse(uri));
199         }
200
201         private String getId() {
202                 String id;
203                 if (issuerInt != null && !issuerInt.equals(""))
204                         id = issuerInt + ":" + label;
205                 else if (issuerExt != null && !issuerExt.equals(""))
206                         id = issuerExt + ":" + label;
207                 else
208                         id = label;
209
210                 return id;
211         }
212
213         public void remove(Context ctx) {
214                 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
215                 prefs.edit().remove(getId()).apply();
216         }
217
218         public void save(Context ctx) {
219                 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
220                 prefs.edit().putString(getId(), toString()).apply();
221         }
222
223         public String getTitle() {
224                 String title = "";
225                 if (issuerExt != null && !issuerExt.equals(""))
226                         title += issuerExt + ": ";
227                 title += label;
228                 return title;
229         }
230
231         public String getCurrentTokenValue(Context ctx, boolean increment) {
232                 if (type == TokenType.HOTP) {
233                         if (increment) {
234                                 try {
235                                         return getHOTP(counter++);
236                                 } finally {
237                                         save(ctx);
238                                 }
239                         } else {
240                                 String placeholder = "";
241                                 for (int i = 0; i < digits; i++)
242                                         placeholder += "-";
243
244                                 return placeholder;
245                         }
246                 }
247
248                 return getHOTP(System.currentTimeMillis() / 1000 / period);
249         }
250
251         public Uri toUri() {
252                 String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label;
253
254                 Uri.Builder builder = new Uri.Builder()
255                         .scheme("otpauth")
256                         .path(issuerLabel)
257                         .appendQueryParameter("secret", Base32String.encode(key))
258                         .appendQueryParameter("issuer", issuerInt == null ? issuerExt : issuerInt)
259                         .appendQueryParameter("algorithm", algo)
260                         .appendQueryParameter("digits", Integer.toString(digits));
261
262                 switch (type) {
263                 case HOTP:
264                         builder.authority("hotp");
265                         builder.appendQueryParameter("counter", Long.toString(counter));
266                         break;
267                 case TOTP:
268                         builder.authority("totp");
269                         builder.appendQueryParameter("period", Integer.toString(period));
270                         break;
271                 }
272
273                 return builder.build();
274         }
275
276         public TokenType getType() {
277                 return type;
278         }
279
280         // Progress is on a scale from 0 - 1000.
281         public int getProgress() {
282                 int p = period * 10;
283
284                 long time = System.currentTimeMillis() / 100;
285                 return (int) ((time % p) * 1000 / p);
286         }
287
288         @Override
289         public String toString() {
290                 return toUri().toString();
291         }
292 }