]> Pileus Git - ~andy/freeotp/blob - src/org/fedorahosted/freeotp/Token.java
35a9576a1539fc1c88d8caa081b4e73bcba04d93
[~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.Locale;
27
28 import javax.crypto.Mac;
29 import javax.crypto.spec.SecretKeySpec;
30
31 import android.net.Uri;
32
33 import com.google.android.apps.authenticator.Base32String;
34 import com.google.android.apps.authenticator.Base32String.DecodingException;
35
36 public class Token {
37         public static class TokenUriInvalidException extends Exception {
38                 private static final long serialVersionUID = -1108624734612362345L;
39         }
40
41         public static enum TokenType {
42                 HOTP, TOTP
43         }
44
45         private final String issuerInt;
46         private final String issuerExt;
47         private final String label;
48         private TokenType type;
49         private String algo;
50         private byte[] key;
51         private int digits;
52         private long counter;
53         private int period;
54
55         private Token(Uri uri) throws TokenUriInvalidException {
56                 if (!uri.getScheme().equals("otpauth"))
57                         throw new TokenUriInvalidException();
58
59                 if (uri.getAuthority().equals("totp"))
60                         type = TokenType.TOTP;
61                 else if (uri.getAuthority().equals("hotp"))
62                         type = TokenType.HOTP;
63                 else
64                         throw new TokenUriInvalidException();
65
66                 String path = uri.getPath();
67                 if (path == null)
68                         throw new TokenUriInvalidException();
69
70                 // Strip the path of its leading '/'
71                 for (int i = 0; path.charAt(i) == '/'; i++)
72                         path = path.substring(1);
73                 if (path.length() == 0)
74                         throw new TokenUriInvalidException();
75
76                 int i = path.indexOf(':');
77                 issuerExt = i < 0 ? "" : path.substring(0, i);
78                 issuerInt = uri.getQueryParameter("issuer");
79                 label = path.substring(i >= 0 ? i + 1 : 0);
80
81                 algo = uri.getQueryParameter("algorithm");
82                 if (algo == null)
83                         algo = "sha1";
84                 algo = algo.toUpperCase(Locale.US);
85                 try {
86                         Mac.getInstance("Hmac" + algo);
87                 } catch (NoSuchAlgorithmException e1) {
88                         throw new TokenUriInvalidException();
89                 }
90
91                 try {
92                         String d = uri.getQueryParameter("digits");
93                         if (d == null)
94                                 d = "6";
95                         digits = Integer.parseInt(d);
96                         if (digits != 6 && digits != 8)
97                                 throw new TokenUriInvalidException();
98                 } catch (NumberFormatException e) {
99                         throw new TokenUriInvalidException();
100                 }
101
102                 switch (type) {
103                 case HOTP:
104                         try {
105                                 String c = uri.getQueryParameter("counter");
106                                 if (c == null)
107                                         c = "0";
108                                 counter = Long.parseLong(c);
109                         } catch (NumberFormatException e) {
110                                 throw new TokenUriInvalidException();
111                         }
112                         break;
113                 case TOTP:
114                         try {
115                                 String p = uri.getQueryParameter("period");
116                                 if (p == null)
117                                         p = "30";
118                                 period = Integer.parseInt(p);
119                         } catch (NumberFormatException e) {
120                                 throw new TokenUriInvalidException();
121                         }
122                         break;
123                 }
124
125                 try {
126                         String s = uri.getQueryParameter("secret");
127                         key = Base32String.decode(s);
128                 } catch (DecodingException e) {
129                         throw new TokenUriInvalidException();
130                 }
131         }
132
133         private String getHOTP(long counter) {
134                 // Encode counter in network byte order
135                 ByteBuffer bb = ByteBuffer.allocate(8);
136                 bb.putLong(counter);
137
138                 // Create digits divisor
139                 int div = 1;
140                 for (int i = digits; i > 0; i--)
141                         div *= 10;
142
143                 // Create the HMAC
144                 try {
145                         Mac mac = Mac.getInstance("Hmac" + algo);
146                         mac.init(new SecretKeySpec(key, "Hmac" + algo));
147
148                         // Do the hashing
149                         byte[] digest = mac.doFinal(bb.array());
150
151                         // Truncate
152                         int binary;
153                         int off = digest[digest.length - 1] & 0xf;
154                         binary  = (digest[off + 0] & 0x7f) << 0x18;
155                         binary |= (digest[off + 1] & 0xff) << 0x10;
156                         binary |= (digest[off + 2] & 0xff) << 0x08;
157                         binary |= (digest[off + 3] & 0xff) << 0x00;
158                         binary  = binary % div;
159
160                         // Zero pad
161                         String hotp = Integer.toString(binary);
162                         while (hotp.length() != digits)
163                                 hotp = "0" + hotp;
164
165                         return hotp;
166                 } catch (InvalidKeyException e) {
167                         e.printStackTrace();
168                 } catch (NoSuchAlgorithmException e) {
169                         e.printStackTrace();
170                 }
171
172                 return "";
173         }
174
175         public Token(String uri) throws TokenUriInvalidException {
176                 this(Uri.parse(uri));
177         }
178
179         public String getID() {
180                 String id;
181                 if (issuerInt != null && !issuerInt.equals(""))
182                         id = issuerInt + ":" + label;
183                 else if (issuerExt != null && !issuerExt.equals(""))
184                         id = issuerExt + ":" + label;
185                 else
186                         id = label;
187
188                 return id;
189         }
190
191         public String getIssuer() {
192                 return issuerExt != null ? issuerExt : "";
193         }
194
195         public String getLabel() {
196                 return label != null ? label : "";
197         }
198
199         public String getCode() {
200                 switch (type) {
201                 case HOTP:
202                         return getHOTP(counter);
203                 case TOTP:
204                         return getHOTP(System.currentTimeMillis() / 1000 / period);
205                 }
206
207                 return null;
208         }
209
210         public String getPlaceholder() {
211                 StringBuilder sb = new StringBuilder(digits);
212                 for (int i = 0; i < digits; i++)
213                         sb.append('-');
214                 return sb.toString();
215         }
216
217         public void increment() {
218                 if (type == TokenType.HOTP)
219                         counter++;
220         }
221
222         public Uri toUri() {
223                 String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label;
224
225                 Uri.Builder builder = new Uri.Builder()
226                         .scheme("otpauth")
227                         .path(issuerLabel)
228                         .appendQueryParameter("secret", Base32String.encode(key))
229                         .appendQueryParameter("issuer", issuerInt == null ? issuerExt : issuerInt)
230                         .appendQueryParameter("algorithm", algo)
231                         .appendQueryParameter("digits", Integer.toString(digits));
232
233                 switch (type) {
234                 case HOTP:
235                         builder.authority("hotp");
236                         builder.appendQueryParameter("counter", Long.toString(counter));
237                         break;
238                 case TOTP:
239                         builder.authority("totp");
240                         builder.appendQueryParameter("period", Integer.toString(period));
241                         break;
242                 }
243
244                 return builder.build();
245         }
246
247         public TokenType getType() {
248                 return type;
249         }
250
251         // Progress is on a scale from 0 - 1000.
252         public int getProgress() {
253                 int p = period * 10;
254
255                 long time = System.currentTimeMillis() / 100;
256                 return (int) ((time % p) * 1000 / p);
257         }
258
259         @Override
260         public String toString() {
261                 return toUri().toString();
262         }
263 }