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