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