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