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