]> Pileus Git - ~andy/freeotp/blob - src/org/fedorahosted/freeotp/Token.java
ca516337785424b6bb1a5875aa48cc0b039db955
[~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                         }
71                 }
72
73                 return tokens;
74         }
75
76         private Token(Uri uri) throws TokenUriInvalidException {
77                 if (!uri.getScheme().equals("otpauth"))
78                         throw new TokenUriInvalidException();
79
80                 if (uri.getAuthority().equals("totp"))
81                         type = TokenType.TOTP;
82                 else if (uri.getAuthority().equals("hotp"))
83                         type = TokenType.HOTP;
84                 else
85                         throw new TokenUriInvalidException();
86
87                 String path = uri.getPath();
88                 if (path == null)
89                         throw new TokenUriInvalidException();
90
91                 // Strip the path of its leading '/'
92                 for (int i = 0; path.charAt(i) == '/'; i++)
93                         path = path.substring(1);
94                 if (path.length() == 0)
95                         throw new TokenUriInvalidException();
96
97                 int i = path.indexOf(':');
98                 issuerExt = i < 0 ? "" : path.substring(0, i);
99                 issuerInt = uri.getQueryParameter("issuer");
100                 label = path.substring(i >= 0 ? i + 1 : 0);
101
102                 algo = uri.getQueryParameter("algorithm");
103                 if (algo == null)
104                         algo = "sha1";
105                 algo = algo.toUpperCase(Locale.US);
106                 try {
107                         Mac.getInstance("Hmac" + algo);
108                 } catch (NoSuchAlgorithmException e1) {
109                         throw new TokenUriInvalidException();
110                 }
111
112                 try {
113                         String d = uri.getQueryParameter("digits");
114                         if (d == null)
115                                 d = "6";
116                         digits = Integer.parseInt(d);
117                         if (digits != 6 && digits != 8)
118                                 throw new TokenUriInvalidException();
119                 } catch (NumberFormatException e) {
120                         throw new TokenUriInvalidException();
121                 }
122
123                 switch (type) {
124                 case HOTP:
125                         try {
126                                 String c = uri.getQueryParameter("counter");
127                                 if (c == null)
128                                         c = "0";
129                                 counter = Long.parseLong(c);
130                         } catch (NumberFormatException e) {
131                                 throw new TokenUriInvalidException();
132                         }
133                         break;
134                 case TOTP:
135                         try {
136                                 String p = uri.getQueryParameter("period");
137                                 if (p == null)
138                                         p = "30";
139                                 period = Integer.parseInt(p);
140                         } catch (NumberFormatException e) {
141                                 throw new TokenUriInvalidException();
142                         }
143                         break;
144                 }
145
146                 try {
147                         String s = uri.getQueryParameter("secret");
148                         key = Base32String.decode(s);
149                 } catch (DecodingException e) {
150                         throw new TokenUriInvalidException();
151                 }
152         }
153
154         private String getHOTP(long counter) {
155                 // Encode counter in network byte order
156                 ByteBuffer bb = ByteBuffer.allocate(8);
157                 bb.putLong(counter);
158
159                 // Create digits divisor
160                 int div = 1;
161                 for (int i = digits; i > 0; i--)
162                         div *= 10;
163
164                 // Create the HMAC
165                 try {
166                         Mac mac = Mac.getInstance("Hmac" + algo);
167                         mac.init(new SecretKeySpec(key, "Hmac" + algo));
168
169                         // Do the hashing
170                         byte[] digest = mac.doFinal(bb.array());
171
172                         // Truncate
173                         int binary;
174                         int off = digest[digest.length - 1] & 0xf;
175                         binary  = (digest[off + 0] & 0x7f) << 0x18;
176                         binary |= (digest[off + 1] & 0xff) << 0x10;
177                         binary |= (digest[off + 2] & 0xff) << 0x08;
178                         binary |= (digest[off + 3] & 0xff) << 0x00;
179                         binary  = binary % div;
180
181                         // Zero pad
182                         String hotp = Integer.toString(binary);
183                         while (hotp.length() != digits)
184                                 hotp = "0" + hotp;
185
186                         return hotp;
187                 } catch (InvalidKeyException e) {
188                         e.printStackTrace();
189                 } catch (NoSuchAlgorithmException e) {
190                         e.printStackTrace();
191                 }
192
193                 return "";
194         }
195
196         public Token(String uri) throws TokenUriInvalidException {
197                 this(Uri.parse(uri));
198         }
199
200         private String getId() {
201                 String id;
202                 if (issuerInt != null && !issuerInt.equals(""))
203                         id = issuerInt + ":" + label;
204                 else if (issuerExt != null && !issuerExt.equals(""))
205                         id = issuerExt + ":" + label;
206                 else
207                         id = label;
208
209                 return id;
210         }
211
212         public void remove(Context ctx) {
213                 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
214                 prefs.edit().remove(getId()).apply();
215         }
216
217         public void save(Context ctx) {
218                 SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
219                 prefs.edit().putString(getId(), toString()).apply();
220         }
221
222         public String getTitle() {
223                 String title = "";
224                 if (issuerExt != null && !issuerExt.equals(""))
225                         title += issuerExt + ": ";
226                 title += label;
227                 return title;
228         }
229
230         public String getCurrentTokenValue(Context ctx, boolean increment) {
231                 if (type == TokenType.HOTP) {
232                         if (increment) {
233                                 try {
234                                         return getHOTP(counter++);
235                                 } finally {
236                                         save(ctx);
237                                 }
238                         } else {
239                                 String placeholder = "";
240                                 for (int i = 0; i < digits; i++)
241                                         placeholder += "-";
242
243                                 return placeholder;
244                         }
245                 }
246
247                 return getHOTP(System.currentTimeMillis() / 1000 / period);
248         }
249
250         public Uri toUri() {
251                 String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label;
252
253                 Uri.Builder builder = new Uri.Builder()
254                         .scheme("otpauth")
255                         .path(issuerLabel)
256                         .appendQueryParameter("secret", Base32String.encode(key))
257                         .appendQueryParameter("issuer", issuerInt == null ? issuerExt : issuerInt)
258                         .appendQueryParameter("algorithm", algo)
259                         .appendQueryParameter("digits", Integer.toString(digits));
260
261                 switch (type) {
262                 case HOTP:
263                         builder.authority("hotp");
264                         builder.appendQueryParameter("counter", Long.toString(counter));
265                         break;
266                 case TOTP:
267                         builder.authority("totp");
268                         builder.appendQueryParameter("period", Integer.toString(period));
269                         break;
270                 }
271
272                 return builder.build();
273         }
274
275         public TokenType getType() {
276                 return type;
277         }
278
279         // Progress is on a scale from 0 - 1000.
280         public int getProgress() {
281                 int p = period * 10;
282
283                 long time = System.currentTimeMillis() / 100;
284                 return (int) ((time % p) * 1000 / p);
285         }
286
287         @Override
288         public String toString() {
289                 return toUri().toString();
290         }
291 }