* Authors: Nathaniel McCallum <npmccallum@redhat.com>
*
* Copyright (C) 2013 Nathaniel McCallum, Red Hat
- * see file 'COPYING' for use and warranty information
*
- * This program is free software you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
*
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
+ * http://www.apache.org/licenses/LICENSE-2.0
*
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package org.fedorahosted.freeotp;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Locale;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
-import android.content.Context;
-import android.content.SharedPreferences;
+import android.content.res.Resources;
import android.net.Uri;
import com.google.android.apps.authenticator.Base32String;
public class Token {
public static class TokenUriInvalidException extends Exception {
private static final long serialVersionUID = -1108624734612362345L;
+ private static int errorResourceID = 0;
+ public TokenUriInvalidException(int id) {
+ this.errorResourceID = id;
+ }
+ public int getErrorResourceID() {
+ return this.errorResourceID;
+ }
}
public static enum TokenType {
HOTP, TOTP
}
- private final String issuerInt;
- private final String issuerExt;
- private final String label;
- private TokenType type;
- private String algo;
- private byte[] key;
- private int digits;
- private long counter;
- private int period;
-
- public static List<Token> getTokens(Context ctx) {
- SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
-
- List<Token> tokens = new ArrayList<Token>();
- for (String key : prefs.getAll().keySet()) {
- try {
- tokens.add(new Token(prefs.getString(key, null)));
- } catch (TokenUriInvalidException e) {
- e.printStackTrace();
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- }
- }
+ private final String mIssuerInt;
+ private final String mIssuerExt;
+ private final String mLabel;
+ private TokenType mType;
+ private String mAlgorithm;
+ private byte[] mSecret;
+ private int mDigits;
+ private long mCounter;
+ private int mPeriod;
+ private long mLastCode;
+
+ private Token(Uri uri) throws TokenUriInvalidException {
+ String scheme = uri.getScheme();
+ String authority = uri.getAuthority();
+ String path = uri.getPath();
- return tokens;
- }
+ if (scheme == null)
+ throw new TokenUriInvalidException(R.string.error_no_scheme);
+ if (authority == null)
+ throw new TokenUriInvalidException(R.string.error_no_authority);
+ if (path == null)
+ throw new TokenUriInvalidException(R.string.error_no_path);
- private Token(Uri uri) throws TokenUriInvalidException, NoSuchAlgorithmException {
- if (!uri.getScheme().equals("otpauth"))
- throw new TokenUriInvalidException();
+ if (!scheme.equals("otpauth"))
+ throw new TokenUriInvalidException(R.string.error_invalid_scheme);
- if (uri.getAuthority().equals("totp"))
- type = TokenType.TOTP;
- else if (uri.getAuthority().equals("hotp"))
- type = TokenType.HOTP;
+ if (authority.equals("totp"))
+ mType = TokenType.TOTP;
+ else if (authority.equals("hotp"))
+ mType = TokenType.HOTP;
else
- throw new TokenUriInvalidException();
-
- String path = uri.getPath();
- if (path == null)
- throw new TokenUriInvalidException();
+ throw new TokenUriInvalidException(R.string.error_invalid_authority);
// Strip the path of its leading '/'
for (int i = 0; path.charAt(i) == '/'; i++)
path = path.substring(1);
if (path.length() == 0)
- throw new TokenUriInvalidException();
+ throw new TokenUriInvalidException(R.string.error_invalid_path);
int i = path.indexOf(':');
- issuerExt = i < 0 ? "" : path.substring(0, i);
- issuerInt = uri.getQueryParameter("issuer");
- label = path.substring(i >= 0 ? i + 1 : 0);
-
- algo = uri.getQueryParameter("algorithm");
- if (algo == null)
- algo = "sha1";
- algo = algo.toUpperCase(Locale.US);
- if (!algo.equals("SHA1") && !algo.equals("SHA256") &&
- !algo.equals("SHA512") && !algo.equals("MD5"))
- throw new TokenUriInvalidException();
- Mac.getInstance("Hmac" + algo);
+ mIssuerExt = i < 0 ? "" : path.substring(0, i);
+ mIssuerInt = uri.getQueryParameter("issuer");
+ mLabel = path.substring(i >= 0 ? i + 1 : 0);
+
+ mAlgorithm = uri.getQueryParameter("algorithm");
+ if (mAlgorithm == null)
+ mAlgorithm = "sha1";
+ mAlgorithm = mAlgorithm.toUpperCase(Locale.US);
+ try {
+ Mac.getInstance("Hmac" + mAlgorithm);
+ } catch (NoSuchAlgorithmException e1) {
+ throw new TokenUriInvalidException(R.string.error_no_algorithm);
+ }
try {
String d = uri.getQueryParameter("digits");
if (d == null)
d = "6";
- digits = Integer.parseInt(d);
- if (digits != 6 && digits != 8)
- throw new TokenUriInvalidException();
+ mDigits = Integer.parseInt(d);
+ if (mDigits != 6 && mDigits != 8)
+ throw new TokenUriInvalidException(R.string.error_invalid_digits);
} catch (NumberFormatException e) {
- throw new TokenUriInvalidException();
+ throw new TokenUriInvalidException(R.string.error_invalid_number);
}
- switch (type) {
+ switch (mType) {
case HOTP:
try {
String c = uri.getQueryParameter("counter");
if (c == null)
c = "0";
- counter = Long.parseLong(c);
+ mCounter = Long.parseLong(c) - 1;
} catch (NumberFormatException e) {
- throw new TokenUriInvalidException();
+ throw new TokenUriInvalidException(R.string.error_invalid_counter);
}
break;
case TOTP:
String p = uri.getQueryParameter("period");
if (p == null)
p = "30";
- period = Integer.parseInt(p);
+ mPeriod = Integer.parseInt(p);
} catch (NumberFormatException e) {
- throw new TokenUriInvalidException();
+ throw new TokenUriInvalidException(R.string.error_invalid_period);
}
break;
}
try {
String s = uri.getQueryParameter("secret");
- key = Base32String.decode(s);
+ mSecret = Base32String.decode(s);
} catch (DecodingException e) {
- throw new TokenUriInvalidException();
+ throw new TokenUriInvalidException(R.string.error_invalid_secret);
}
}
// Create digits divisor
int div = 1;
- for (int i = digits; i > 0; i--)
+ for (int i = mDigits; i > 0; i--)
div *= 10;
// Create the HMAC
try {
- Mac mac = Mac.getInstance("Hmac" + algo);
- mac.init(new SecretKeySpec(key, "Hmac" + algo));
+ Mac mac = Mac.getInstance("Hmac" + mAlgorithm);
+ mac.init(new SecretKeySpec(mSecret, "Hmac" + mAlgorithm));
// Do the hashing
byte[] digest = mac.doFinal(bb.array());
// Zero pad
String hotp = Integer.toString(binary);
- while (hotp.length() != digits)
+ while (hotp.length() != mDigits)
hotp = "0" + hotp;
return hotp;
return "";
}
- public Token(String uri) throws TokenUriInvalidException, NoSuchAlgorithmException {
+ public Token(String uri) throws TokenUriInvalidException {
this(Uri.parse(uri));
}
- private String getId() {
+ public void increment() {
+ if (mType == TokenType.HOTP) {
+ mCounter++;
+ mLastCode = System.currentTimeMillis();
+ }
+ }
+
+ public String getID() {
String id;
- if (issuerInt != null && !issuerInt.equals(""))
- id = issuerInt + ":" + label;
- else if (issuerExt != null && !issuerExt.equals(""))
- id = issuerExt + ":" + label;
+ if (mIssuerInt != null && !mIssuerInt.equals(""))
+ id = mIssuerInt + ":" + mLabel;
+ else if (mIssuerExt != null && !mIssuerExt.equals(""))
+ id = mIssuerExt + ":" + mLabel;
else
- id = label;
+ id = mLabel;
return id;
}
- public void remove(Context ctx) {
- SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
- prefs.edit().remove(getId()).apply();
+ public String getIssuer() {
+ return mIssuerExt != null ? mIssuerExt : "";
}
- public void save(Context ctx) {
- SharedPreferences prefs = ctx.getSharedPreferences(Token.class.getName(), Context.MODE_PRIVATE);
- prefs.edit().putString(getId(), toString()).apply();
+ public String getLabel() {
+ return mLabel != null ? mLabel : "";
}
- public String getTitle() {
- String title = "";
- if (issuerExt != null && !issuerExt.equals(""))
- title += issuerExt + ": ";
- title += label;
- return title;
- }
+ public String getCode() {
+ if (mType == TokenType.TOTP)
+ return getHOTP(System.currentTimeMillis() / 1000 / mPeriod);
- public String getCurrentTokenValue(Context ctx, boolean increment) {
- if (type == TokenType.HOTP) {
- if (increment) {
- try {
- return getHOTP(counter++);
- } finally {
- save(ctx);
- }
- } else {
- String placeholder = "";
- for (int i = 0; i < digits; i++)
- placeholder += "-";
-
- return placeholder;
- }
+ long time = System.currentTimeMillis();
+ if (time - mLastCode > 60000) {
+ StringBuilder sb = new StringBuilder(mDigits);
+ for (int i = 0; i < mDigits; i++)
+ sb.append('-');
+ return sb.toString();
}
- return getHOTP(System.currentTimeMillis() / 1000 / period);
+ return getHOTP(mCounter);
+ }
+
+ public TokenType getType() {
+ return mType;
+ }
+
+ // Progress is on a scale from 0 - 1000.
+ public int getProgress() {
+ long time = System.currentTimeMillis();
+
+ if (mType == TokenType.TOTP)
+ return 1000 - (int) (time % (mPeriod * 1000) / mPeriod);
+
+ long state = (time - mLastCode) / 60;
+ return 1000 - (int) (state > 1000 ? 1000 : state);
}
public Uri toUri() {
- String issuerLabel = !issuerExt.equals("") ? issuerExt + ":" + label : label;
+ String issuerLabel = !mIssuerExt.equals("") ? mIssuerExt + ":" + mLabel : mLabel;
Uri.Builder builder = new Uri.Builder()
.scheme("otpauth")
.path(issuerLabel)
- .appendQueryParameter("secret", Base32String.encode(key))
- .appendQueryParameter("issuer", issuerInt == null ? issuerExt : issuerInt)
- .appendQueryParameter("algorithm", algo)
- .appendQueryParameter("digits", Integer.toString(digits));
+ .appendQueryParameter("secret", Base32String.encode(mSecret))
+ .appendQueryParameter("issuer", mIssuerInt == null ? mIssuerExt : mIssuerInt)
+ .appendQueryParameter("algorithm", mAlgorithm)
+ .appendQueryParameter("digits", Integer.toString(mDigits));
- switch (type) {
+ switch (mType) {
case HOTP:
builder.authority("hotp");
- builder.appendQueryParameter("counter", Long.toString(counter));
+ builder.appendQueryParameter("counter", Long.toString(mCounter + 1));
break;
case TOTP:
builder.authority("totp");
- builder.appendQueryParameter("period", Integer.toString(period));
+ builder.appendQueryParameter("period", Integer.toString(mPeriod));
break;
}
return builder.build();
}
- public TokenType getType() {
- return type;
- }
-
- // Progress is on a scale from 0 - 1000.
- public int getProgress() {
- int p = period * 10;
-
- long time = System.currentTimeMillis() / 100;
- return (int) ((time % p) * 1000 / p);
- }
-
@Override
public String toString() {
return toUri().toString();