]> Pileus Git - ~andy/freeotp/blobdiff - src/org/fedorahosted/freeotp/Token.java
Add detailed invalid URI messages
[~andy/freeotp] / src / org / fedorahosted / freeotp / Token.java
index a42d36f92ec4c6783c4dd030358b1e758f82604a..0e111e2264eb07bcf9ecb6c5e3d0c3c7a1cc209f 100644 (file)
@@ -4,20 +4,18 @@
  * 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;
@@ -25,15 +23,12 @@ 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;
@@ -42,94 +37,93 @@ import com.google.android.apps.authenticator.Base32String.DecodingException;
 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:
@@ -137,18 +131,18 @@ public class Token {
                                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);
                }
        }
 
@@ -159,13 +153,13 @@ public class Token {
 
                // 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());
@@ -181,7 +175,7 @@ public class Token {
 
                        // Zero pad
                        String hotp = Integer.toString(binary);
-                       while (hotp.length() != digits)
+                       while (hotp.length() != mDigits)
                                hotp = "0" + hotp;
 
                        return hotp;
@@ -194,97 +188,92 @@ public class Token {
                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();