- Include OTP NFC client library
- Add NfcHelpers to implement common NFC tasks
- Add ExternalToken implementation of Token
- Add ExternalTokenPersistence implementation of TokenPersistence
- Update interfaces to include new functionality
- Update other classes to support new interfaces
---
app/build.gradle | 6 +
app/src/main/AndroidManifest.xml | 2 +
.../org/fedorahosted/freeotp/ExternalToken.java | 130 +++++++++++++++++++++
.../freeotp/ExternalTokenPersistence.java | 129 ++++++++++++++++++++
.../org/fedorahosted/freeotp/InternalToken.java | 16 +++
.../freeotp/InternalTokenPersistence.java | 20 +++-
.../java/org/fedorahosted/freeotp/NfcHelpers.java | 60 ++++++++++
.../main/java/org/fedorahosted/freeotp/Token.java | 9 +-
.../org/fedorahosted/freeotp/TokenAdapter.java | 28 +++--
.../org/fedorahosted/freeotp/TokenPersistence.java | 14 +--
.../fedorahosted/freeotp/edit/DeleteActivity.java | 4 +-
.../fedorahosted/freeotp/edit/EditActivity.java | 4 +-
app/src/main/res/values/strings.xml | 3 +-
13 files changed, 389 insertions(+), 36 deletions(-)
create mode 100644 app/src/main/java/org/fedorahosted/freeotp/ExternalToken.java
create mode 100644
app/src/main/java/org/fedorahosted/freeotp/ExternalTokenPersistence.java
create mode 100644 app/src/main/java/org/fedorahosted/freeotp/NfcHelpers.java
diff --git a/app/build.gradle b/app/build.gradle
index 0faef46..58499f3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,9 +1,15 @@
+apply plugin: 'maven'
apply plugin: 'com.android.application'
+repositories {
+ maven { url 'http://releases.marmeladburk.fidesmo.com/' }
+}
+
dependencies {
compile 'com.google.zxing:core:3.1.0'
compile 'com.google.code.gson:gson:2.3'
compile 'com.squareup.picasso:picasso:2.3.3'
+ compile 'com.fidesmo:otp-client:0.0.2'
}
android {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fd6c7a5..0eeaa19 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -38,9 +38,11 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
/>
<uses-feature android:name="android.hardware.camera"
android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus"
android:required="false" />
+ <uses-feature android:name="android.hardware.nfc"
android:required="false" />
<application
android:allowBackup="true"
diff --git a/app/src/main/java/org/fedorahosted/freeotp/ExternalToken.java
b/app/src/main/java/org/fedorahosted/freeotp/ExternalToken.java
new file mode 100644
index 0000000..0aaec1f
--- /dev/null
+++ b/app/src/main/java/org/fedorahosted/freeotp/ExternalToken.java
@@ -0,0 +1,130 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Petter Arvidsson <petter.arvidsson(a)fidesmo.com>
+ *
+ * Copyright (C) 2014 Petter Arvidsson, Fidesmo AB
+ *
+ * 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
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.io.IOException;
+import android.net.Uri;
+import com.fidesmo.oath.hardware.HardwareToken;
+import com.fidesmo.oath.hardware.TokenMeta;
+
+public class ExternalToken implements Token {
+ private String issuer;
+ private String label;
+ private String id;
+ private TokenType type;
+ private int digits;
+ private String algorithm;
+ private int period = 30;
+
+ public ExternalToken(TokenMeta meta) {
+ this.id = meta.getLabel();
+ String s[] = meta.getLabel().split(":", 2);
+ if(s.length == 1) {
+ label = s[0];
+ } else {
+ issuer = s[0];
+ label = s[1];
+ }
+
+ switch(meta.getType()) {
+ case TOTP:
+ this.type = TokenType.TOTP;
+ break;
+ case HOTP:
+ this.type = TokenType.HOTP;
+ break;
+ }
+ switch(meta.getAlgorithm()) {
+ case SHA1:
+ algorithm = "SHA1";
+ break;
+ case SHA256:
+ algorithm = "SHA256";
+ break;
+ }
+ this.digits = meta.getDigits();
+ }
+
+ public String getID() {
+ return id;
+ }
+
+ public String getIssuer() {
+ return issuer != null ? issuer : "";
+ }
+
+ public void setIssuer(String issuer) {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getDigits() {
+ return digits;
+ }
+
+ private TokenCode generateTotpCode(HardwareToken token, long counter, TokenCode next)
throws IOException {
+ return new TokenCode(token.readTotpCode(id, counter), counter * period * 1000,
(counter + 1) * period * 1000, next);
+ }
+
+ private TokenCode generateHotpCode(HardwareToken token, long cur) throws IOException
{
+ return new TokenCode(token.readHotpCode(id), cur, cur + (period * 1000));
+ }
+
+ public TokenCode generateCodes(HardwareToken token) throws IOException {
+ long cur = System.currentTimeMillis();
+ switch(type) {
+ case TOTP:
+ long counter = cur / 1000 / period;
+ return generateTotpCode(token, counter, generateTotpCode(token, counter + 1,
null));
+ case HOTP:
+ return generateHotpCode(token, cur);
+ default:
+ throw new IOException("Invalid token type");
+ }
+ }
+
+ public TokenType getType() {
+ return type;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public Uri getImage() {
+ return null;
+ }
+
+ public void setImage(Uri image) {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean isInternal() {
+ return false;
+ }
+}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/ExternalTokenPersistence.java
b/app/src/main/java/org/fedorahosted/freeotp/ExternalTokenPersistence.java
new file mode 100644
index 0000000..d9ded71
--- /dev/null
+++ b/app/src/main/java/org/fedorahosted/freeotp/ExternalTokenPersistence.java
@@ -0,0 +1,129 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Petter Arvidsson <petter.arvidsson(a)fidesmo.com>
+ *
+ * Copyright (C) 2014 Petter Arvidsson, Fidesmo AB
+ *
+ * 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
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.util.*;
+import java.io.IOException;
+import com.fidesmo.oath.hardware.*;
+import com.yubico.yubioath.model.*;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.widget.Toast;
+import android.nfc.tech.IsoDep;
+
+public class ExternalTokenPersistence implements TokenPersistence {
+ HardwareToken store;
+ byte[] id;
+ List<TokenMeta> tokens;
+
+ public ExternalTokenPersistence(IsoDep card) throws IOException{
+ store = new YkneoOath(card);
+ long timestamp = (System.currentTimeMillis() / 1000 + 10) / 30;
+ store.open();
+ tokens = store.getTokens(timestamp);
+ store.close();
+ }
+
+ public Token addWithToast(Context ctx, String uri) {
+ try {
+ InternalToken token = new InternalToken(uri);
+ add(token);
+ return token;
+ } catch (Token.TokenUriInvalidException e) {
+ Toast.makeText(ctx, R.string.invalid_token, Toast.LENGTH_SHORT).show();
+ } catch (IOException e) {
+ Toast.makeText(ctx, R.string.failed_to_read_external_token,
Toast.LENGTH_SHORT).show();
+ }
+ return null;
+
+ }
+
+ public int length() {
+ return tokens.size();
+ }
+
+ public Token get(int position) {
+ return new ExternalToken(tokens.get(position));
+ }
+
+ private static TokenMeta tokenMeta(InternalToken token) {
+ TokenMeta.Type type = null;
+ switch(token.getType()) {
+ case HOTP:
+ type = TokenMeta.Type.HOTP;
+ break;
+ case TOTP:
+ type = TokenMeta.Type.TOTP;
+ break;
+ }
+
+ if(token.getAlgorithm().equals("SHA1")) {
+ return new TokenMeta(token.getID(), token.getDigits(), type,
TokenMeta.Algorithm.SHA1);
+ } else if(token.getAlgorithm().equals("SHA256")) {
+ return new TokenMeta(token.getID(), token.getDigits(), type,
TokenMeta.Algorithm.SHA256);
+ } else {
+ return null;
+ }
+ }
+
+ public void add(InternalToken token) throws Token.TokenUriInvalidException,
IOException {
+ try {
+ store.open();
+ if(token.getType() == Token.TokenType.HOTP)
+ store.storeCode(tokenMeta(token), token.getSecret(),
(int)token.getCounter());
+ else
+ store.storeCode(tokenMeta(token), token.getSecret(), token.getPeriod());
+ long timestamp = (System.currentTimeMillis() / 1000 + 10) / 30;
+ tokens = store.getTokens(timestamp);
+ } finally {
+ store.close();
+ }
+ }
+
+ public void move(int fromPosition, int toPosition) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void delete(int position) throws IOException {
+ try {
+ Token token = new ExternalToken(tokens.get(position));
+ store.open();
+ store.deleteCode(token.getID());
+ long timestamp = (System.currentTimeMillis() / 1000 + 10) / 30;
+ tokens = store.getTokens(timestamp);
+ } finally {
+ store.close();
+ }
+ }
+
+ public TokenCode generateCodes(int position) throws IOException {
+ try {
+ store.open();
+ return new ExternalToken(tokens.get(position)).generateCodes(store);
+ } finally {
+ store.close();
+ }
+ }
+
+ public boolean isMovable() {
+ return false;
+ }
+}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/InternalToken.java
b/app/src/main/java/org/fedorahosted/freeotp/InternalToken.java
index 103f21a..5486c14 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/InternalToken.java
+++ b/app/src/main/java/org/fedorahosted/freeotp/InternalToken.java
@@ -296,6 +296,22 @@ public class InternalToken implements Token {
return null;
}
+ public byte[] getSecret() {
+ return secret;
+ }
+
+ public long getCounter() {
+ return counter;
+ }
+
+ public int getPeriod() {
+ return period;
+ }
+
+ public String getAlgorithm() {
+ return algo;
+ }
+
public boolean isInternal() {
return true;
}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/InternalTokenPersistence.java
b/app/src/main/java/org/fedorahosted/freeotp/InternalTokenPersistence.java
index 883a139..81dd73e 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/InternalTokenPersistence.java
+++ b/app/src/main/java/org/fedorahosted/freeotp/InternalTokenPersistence.java
@@ -54,7 +54,7 @@ public class InternalTokenPersistence implements TokenPersistence {
InternalToken token = new InternalToken(uri);
add(token);
return token;
- } catch (InternalToken.TokenUriInvalidException e) {
+ } catch (Token.TokenUriInvalidException e) {
Toast.makeText(ctx, R.string.invalid_token, Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
@@ -71,7 +71,7 @@ public class InternalTokenPersistence implements TokenPersistence {
return getTokenOrder().size();
}
- public InternalToken get(int position) {
+ private InternalToken getInternalToken(int position) {
String key = getTokenOrder().get(position);
String str = prefs.getString(key, null);
@@ -81,15 +81,18 @@ public class InternalTokenPersistence implements TokenPersistence {
// Backwards compatibility for URL-based persistence.
try {
return new InternalToken(str, true);
- } catch (InternalToken.TokenUriInvalidException tuie) {
+ } catch (Token.TokenUriInvalidException tuie) {
tuie.printStackTrace();
}
}
-
return null;
}
- public void add(InternalToken token) throws InternalToken.TokenUriInvalidException {
+ public Token get(int position) {
+ return getInternalToken(position);
+ }
+
+ public void add(InternalToken token) throws Token.TokenUriInvalidException {
String key = token.getID();
if (prefs.contains(key))
@@ -124,6 +127,13 @@ public class InternalTokenPersistence implements TokenPersistence {
prefs.edit().putString(token.getID(), gson.toJson(token)).apply();
}
+ public TokenCode generateCodes(int position) {
+ InternalToken token = getInternalToken(position);
+ TokenCode codes = token.generateCodes();
+ save(token);
+ return codes;
+ }
+
public boolean isMovable() {
return true;
}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/NfcHelpers.java
b/app/src/main/java/org/fedorahosted/freeotp/NfcHelpers.java
new file mode 100644
index 0000000..8a809cb
--- /dev/null
+++ b/app/src/main/java/org/fedorahosted/freeotp/NfcHelpers.java
@@ -0,0 +1,60 @@
+/*
+ * FreeOTP
+ *
+ * Authors: Petter Arvidsson <petter.arvidsson(a)fidesmo.com>
+ *
+ * Copyright (C) 2014 Petter Arvidsson, Fidesmo AB
+ *
+ * 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
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nfc.NfcAdapter;
+import android.nfc.tech.IsoDep;
+import android.nfc.Tag;
+
+public class NfcHelpers {
+
+ public static void enableForegroundDispatch(Activity activity, NfcAdapter adapter) {
+ Intent intent = activity.getIntent();
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ enableForegroundDispatch(activity, adapter, intent);
+ }
+
+ public static void enableForegroundDispatch(Activity activity, NfcAdapter adapter,
Intent intent) {
+ if(adapter.isEnabled()) {
+ PendingIntent tagIntent = PendingIntent.getActivity(activity, 0, intent,
PendingIntent.FLAG_CANCEL_CURRENT);
+ IntentFilter iso = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
+ adapter.enableForegroundDispatch(activity, tagIntent, new
IntentFilter[]{iso},
+ new String[][]{new
String[]{IsoDep.class.getName()}});
+ }
+ }
+
+ public static void disableForegroundDispatch(Activity activity, NfcAdapter adapter)
{
+ adapter.disableForegroundDispatch(activity);
+ }
+
+ public static IsoDep getIsoTag(Intent intent) {
+ Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+ if(tag != null) {
+ return IsoDep.get(tag);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/Token.java
b/app/src/main/java/org/fedorahosted/freeotp/Token.java
index 2709bd2..eaa8ab1 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/Token.java
+++ b/app/src/main/java/org/fedorahosted/freeotp/Token.java
@@ -20,9 +20,11 @@
package org.fedorahosted.freeotp;
+import java.io.IOException;
+import java.io.Serializable;
import android.net.Uri;
-public interface Token {
+public interface Token extends Serializable {
public static class TokenUriInvalidException extends Exception {
private static final long serialVersionUID = -1108624734612362345L;
}
@@ -43,14 +45,13 @@ public interface Token {
public int getDigits();
- // NOTE: This may change internal data. You MUST save the token immediately.
- public TokenCode generateCodes();
-
public TokenType getType();
public Uri getImage();
public void setImage(Uri image);
+ public String getAlgorithm();
+
public boolean isInternal();
}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/TokenAdapter.java
b/app/src/main/java/org/fedorahosted/freeotp/TokenAdapter.java
index 2e521f8..94baaab 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/TokenAdapter.java
+++ b/app/src/main/java/org/fedorahosted/freeotp/TokenAdapter.java
@@ -37,6 +37,7 @@ import org.fedorahosted.freeotp.edit.EditActivity;
import java.util.HashMap;
import java.util.Map;
+import java.io.IOException;
public class TokenAdapter extends BaseReorderableAdapter {
private final TokenPersistence mTokenPersistence;
@@ -121,17 +122,22 @@ public class TokenAdapter extends BaseReorderableAdapter {
// Increment the token.
Token token = tp.get(position);
- TokenCode codes = token.generateCodes();
- tp.save(token);
-
- // Copy code to clipboard.
- mClipMan.setPrimaryClip(ClipData.newPlainText(null,
codes.getCurrentCode()));
- Toast.makeText(v.getContext().getApplicationContext(),
- R.string.code_copied,
- Toast.LENGTH_SHORT).show();
-
- mTokenCodes.put(token.getID(), codes);
- ((TokenLayout) v).start(token.getType(), codes, true);
+ try {
+ TokenCode codes = tp.generateCodes(position);
+
+ // Copy code to clipboard.
+ mClipMan.setPrimaryClip(ClipData.newPlainText(null,
codes.getCurrentCode()));
+ Toast.makeText(v.getContext().getApplicationContext(),
+ R.string.code_copied,
+ Toast.LENGTH_SHORT).show();
+
+ mTokenCodes.put(token.getID(), codes);
+ ((TokenLayout) v).start(token.getType(), codes, true);
+ } catch(IOException e) {
+ Toast.makeText(v.getContext().getApplicationContext(),
+ R.string.external_token_needed,
+ Toast.LENGTH_SHORT).show();
+ }
}
});
diff --git a/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java
b/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java
index 889b9fb..84ab32e 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java
+++ b/app/src/main/java/org/fedorahosted/freeotp/TokenPersistence.java
@@ -19,22 +19,14 @@
*/
package org.fedorahosted.freeotp;
-import java.lang.reflect.Type;
-import java.util.LinkedList;
-import java.util.List;
-
+import java.io.IOException;
import android.content.Context;
-import android.content.SharedPreferences;
-
-import com.google.gson.reflect.TypeToken;
public interface TokenPersistence {
- public Token addWithToast(Context ctx, String uri);
public int length();
public Token get(int position);
- public void add(InternalToken token) throws Token.TokenUriInvalidException;
+ public void add(InternalToken token) throws Token.TokenUriInvalidException,
IOException;
public void move(int fromPosition, int toPosition);
- public void delete(int position);
- public void save(Token token);
+ public TokenCode generateCodes(int position) throws IOException;
public boolean isMovable();
}
diff --git a/app/src/main/java/org/fedorahosted/freeotp/edit/DeleteActivity.java
b/app/src/main/java/org/fedorahosted/freeotp/edit/DeleteActivity.java
index edad048..7da16fb 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/edit/DeleteActivity.java
+++ b/app/src/main/java/org/fedorahosted/freeotp/edit/DeleteActivity.java
@@ -1,7 +1,7 @@
package org.fedorahosted.freeotp.edit;
import org.fedorahosted.freeotp.R;
-import org.fedorahosted.freeotp.InternalToken;
+import org.fedorahosted.freeotp.Token;
import org.fedorahosted.freeotp.InternalTokenPersistence;
import android.os.Bundle;
@@ -18,7 +18,7 @@ public class DeleteActivity extends BaseActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.delete);
mTokenPersistence = new InternalTokenPersistence(this);
- InternalToken token = mTokenPersistence.get(getPosition());
+ Token token = mTokenPersistence.get(getPosition());
((TextView) findViewById(R.id.issuer)).setText(token.getIssuer());
((TextView) findViewById(R.id.label)).setText(token.getLabel());
Picasso.with(this)
diff --git a/app/src/main/java/org/fedorahosted/freeotp/edit/EditActivity.java
b/app/src/main/java/org/fedorahosted/freeotp/edit/EditActivity.java
index 5f61379..d55a657 100644
--- a/app/src/main/java/org/fedorahosted/freeotp/edit/EditActivity.java
+++ b/app/src/main/java/org/fedorahosted/freeotp/edit/EditActivity.java
@@ -75,7 +75,7 @@ public class EditActivity extends BaseActivity implements TextWatcher,
View.OnCl
// Get token values.
mTokenPersistence = new InternalTokenPersistence(this);
- InternalToken token = mTokenPersistence.get(getPosition());
+ InternalToken token = (InternalToken)mTokenPersistence.get(getPosition());
mIssuerCurrent = token.getIssuer();
mLabelCurrent = token.getLabel();
mImageCurrent = token.getImage();
@@ -152,7 +152,7 @@ public class EditActivity extends BaseActivity implements TextWatcher,
View.OnCl
break;
case R.id.save:
- InternalToken token = mTokenPersistence.get(getPosition());
+ InternalToken token =
(InternalToken)mTokenPersistence.get(getPosition());
token.setIssuer(mIssuer.getText().toString());
token.setLabel(mLabel.getText().toString());
token.setImage(mImageDisplay);
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index de39f41..359026f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -37,7 +37,8 @@
<string name="delete_summary">This is your last chance: if you delete
this token, it will be gone forever. It will not be disabled on the
server.</string>
<string name="delete_question">Delete this token?</string>
-
+ <string name="external_token_needed">You need the NFC device to
generate a code</string>
+ <string name="failed_to_read_external_token">Failed to read the NFC
device</string>
<string-array name="algorithms">
<item>MD5</item>
<item>SHA1</item>
--
1.9.1