From faec1dfd8804cb6ebc86f23a2b6e38c795d786c3 Mon Sep 17 00:00:00 2001 From: Bruno Bierbaumer Date: Mon, 23 Nov 2015 22:20:14 +0100 Subject: [PATCH] initial code dump --- .idea/.name | 1 + .idea/compiler.xml | 22 + .idea/copyright/profiles_settings.xml | 3 + .idea/gradle.xml | 18 + .idea/misc.xml | 35 ++ .idea/modules.xml | 9 + .idea/runConfigurations.xml | 12 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle | 45 ++ app/proguard-rules.pro | 17 + app/src/androidTest/AndroidManifest.xml | 7 + .../otp_authenticator/ApplicationTest.java | 158 ++++++ .../otp_authenticator/MainActivityTest.java | 483 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 29 ++ app/src/main/ic_launcher-web.png | Bin 0 -> 28962 bytes .../CaptureActivityAnyOrientation.java | 7 + .../otp_authenticator/EncryptionHelper.java | 96 ++++ .../otp_authenticator/EntriesAdapter.java | 134 +++++ .../bierbaumer/otp_authenticator/Entry.java | 105 ++++ .../otp_authenticator/MainActivity.java | 281 ++++++++++ .../otp_authenticator/SecretKeyWrapper.java | 119 +++++ .../otp_authenticator/SettingsHelper.java | 68 +++ .../otp_authenticator/TOTPHelper.java | 52 ++ .../bierbaumer/otp_authenticator/Utils.java | 51 ++ app/src/main/res/layout/activity_main.xml | 58 +++ app/src/main/res/layout/content_main.xml | 30 ++ app/src/main/res/layout/dialog_about.xml | 7 + app/src/main/res/layout/row.xml | 29 ++ app/src/main/res/menu/menu_edit.xml | 18 + app/src/main/res/menu/menu_main.xml | 16 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2923 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1822 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4158 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6487 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9411 bytes app/src/main/res/raw/about.html | 27 + app/src/main/res/raw/opensource.html | 31 ++ app/src/main/res/values-v21/styles.xml | 10 + app/src/main/res/values-w820dp/dimens.xml | 6 + app/src/main/res/values/colors.xml | 11 + app/src/main/res/values/dimens.xml | 6 + app/src/main/res/values/strings.xml | 7 + app/src/main/res/values/styles.xml | 19 + build.gradle | 23 + gradle.properties | 18 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53637 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 ++++++ gradlew.bat | 90 ++++ settings.gradle | 1 + 51 files changed, 2332 insertions(+) create mode 100644 .idea/.name create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/AndroidManifest.xml create mode 100644 app/src/androidTest/java/net/bierbaumer/otp_authenticator/ApplicationTest.java create mode 100644 app/src/androidTest/java/net/bierbaumer/otp_authenticator/MainActivityTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-web.png create mode 100644 app/src/main/java/net/bierbaumer/otp_authenticator/CaptureActivityAnyOrientation.java create mode 100644 app/src/main/java/net/bierbaumer/otp_authenticator/EncryptionHelper.java create mode 100644 app/src/main/java/net/bierbaumer/otp_authenticator/EntriesAdapter.java create mode 100644 app/src/main/java/net/bierbaumer/otp_authenticator/Entry.java create mode 100644 app/src/main/java/net/bierbaumer/otp_authenticator/MainActivity.java create mode 100644 app/src/main/java/net/bierbaumer/otp_authenticator/SecretKeyWrapper.java create mode 100644 app/src/main/java/net/bierbaumer/otp_authenticator/SettingsHelper.java create mode 100644 app/src/main/java/net/bierbaumer/otp_authenticator/TOTPHelper.java create mode 100644 app/src/main/java/net/bierbaumer/otp_authenticator/Utils.java create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/content_main.xml create mode 100644 app/src/main/res/layout/dialog_about.xml create mode 100644 app/src/main/res/layout/row.xml create mode 100644 app/src/main/res/menu/menu_edit.xml create mode 100644 app/src/main/res/menu/menu_main.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/raw/about.html create mode 100644 app/src/main/res/raw/opensource.html create mode 100644 app/src/main/res/values-v21/styles.xml create mode 100644 app/src/main/res/values-w820dp/dimens.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..94112253 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +otp-authenticator \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..96cc43ef --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..e7bedf33 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..8d2df476 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..efbef40d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + 1.8 + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..35b45b23 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 00000000..7f68460d --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..6564d52d --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..556b0f1f --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,45 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + applicationId "net.bierbaumer.otp_authenticator" + minSdkVersion 15 + targetSdkVersion 23 + versionCode 1 + versionName "0.1" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + debug { + testCoverageEnabled = true + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:design:23.1.1' + compile 'com.journeyapps:zxing-android-embedded:3.0.3@aar' + compile 'com.google.zxing:core:3.2.0' + compile 'commons-codec:commons-codec:1.5' + + androidTestCompile 'com.android.support:support-annotations:23.1.1' + androidTestCompile 'com.android.support.test:runner:0.4.1' + androidTestCompile 'com.android.support.test:rules:0.4.1' + androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' + androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1' + androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1' + + androidTestCompile 'org.hamcrest:hamcrest-library:1.3' + +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..d12ff2e1 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/bb/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/AndroidManifest.xml b/app/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000..f326dc69 --- /dev/null +++ b/app/src/androidTest/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/androidTest/java/net/bierbaumer/otp_authenticator/ApplicationTest.java b/app/src/androidTest/java/net/bierbaumer/otp_authenticator/ApplicationTest.java new file mode 100644 index 00000000..7e9f1b73 --- /dev/null +++ b/app/src/androidTest/java/net/bierbaumer/otp_authenticator/ApplicationTest.java @@ -0,0 +1,158 @@ +package net.bierbaumer.otp_authenticator; + +import android.app.Application; +import android.content.Context; +import android.test.ApplicationTestCase; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base32; +import org.apache.commons.codec.binary.Hex; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } + + public void testTOTPHelper(){ + byte[] b = "12345678901234567890".getBytes(); + assertEquals(94287082, TOTPHelper.generate(b, 59l, 8)); + assertEquals(7081804, TOTPHelper.generate(b, 1111111109l, 8)); + assertEquals(14050471, TOTPHelper.generate(b, 1111111111l, 8)); + assertEquals(89005924, TOTPHelper.generate(b, 1234567890l, 8)); + assertEquals(69279037, TOTPHelper.generate(b, 2000000000l, 8)); + assertEquals(65353130, TOTPHelper.generate(b, 20000000000l, 8)); + } + + + public void testEntry() throws JSONException { + byte secret[] = "Das System ist sicher".getBytes(); + String label = "5 von 5 Sterne"; + + String s = "{\"secret\":\""+ new String(new Base32().encode(secret)) +"\",\"label\":\"" + label + "\"}"; + + Entry e = new Entry(new JSONObject(s)); + assertTrue(Arrays.equals(secret, e.getSecret())); + assertEquals(label, e.getLabel()); + + assertEquals(s, e.toJSON()+""); + } + + + + public void testEntryURL() throws Exception { + try { + new Entry("DON'T CARE"); + assertTrue(false); + } catch (Exception e) { + } + + try { + new Entry("https://github.com/0xbb/"); + assertTrue(false); + } catch (Exception e) { + } + + try { + new Entry("otpauth://hotp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"); + assertTrue(false); + } + catch (Exception e){ + } + + try { + new Entry("otpauth://totp/ACME"); + assertTrue(false); + } + catch (Exception e){ + } + + Entry entry = new Entry("otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&ALGORITHM=SHA1&digits=6&period=30"); + assertEquals("ACME Co - ACME Co:john.doe@email.com", entry.getLabel()); + + assertEquals("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", new String(new Base32().encode(entry.getSecret()))); + } + + public void testSettingsHelper() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { + Context context = getContext(); + + final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + keyStore.deleteEntry("settings"); + + new File(context.getFilesDir() + "/" + SettingsHelper.SETTINGS_FILE).delete(); + new File(context.getFilesDir() + "/" + SettingsHelper.KEY_FILE).delete(); + + ArrayList b = SettingsHelper.load(context); + assertEquals(0, b.size()); + + + ArrayList a = new ArrayList<>(); + Entry e = new Entry(); + e.setLabel("label"); + e.setSecret("secret".getBytes()); + a.add(e); + + e = new Entry(); + e.setLabel("label2"); + e.setSecret("secret2".getBytes()); + a.add(e); + + SettingsHelper.store(context, a); + b = SettingsHelper.load(context); + + assertEquals(a, b); + + new File(context.getFilesDir() + "/" + SettingsHelper.SETTINGS_FILE).delete(); + new File(context.getFilesDir() + "/" + SettingsHelper.KEY_FILE).delete(); + } + + public void testEncryptionHelper() throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException, DecoderException { + + + // https://golang.org/src/crypto/cipher/gcm_test.go + String[][] testCases = new String[][]{ + new String []{"11754cd72aec309bf52f7687212e8957","3c819d9a9bed087615030b65","", "250327c674aaf477aef2675748cf6971" }, + new String []{"ca47248ac0b6f8372a97ac43508308ed","ffd2b598feabc9019262d2be","", "60d20404af527d248d893ae495707d1a" }, + new String []{"7fddb57453c241d03efbed3ac44e371c","ee283a3fc75575e33efd4887","d5de42b461646c255c87bd2962d3b9a2", "2ccda4a5415cb91e135c2a0f78c9b2fdb36d1df9b9d5e596f83e8b7f52971cb3" }, + new String []{"ab72c77b97cb5fe9a382d9fe81ffdbed","54cc7dc2c37ec006bcc6d1da","007c5e5b3e59df24a7c355584fc1518d", "0e1bde206a07a9c2c1b65300f8c649972b4401346697138c7a4891ee59867d0c" }, + new String []{"feffe9928665731c6d6a8f9467308308","cafebabefacedbaddecaf888","d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f59854d5c2af327cd64a62cf35abd2ba6fab4" }, + + }; + + for(String[] testCase: testCases){ + + SecretKeySpec k = new SecretKeySpec(new Hex().decode(testCase[0].getBytes()), "AES"); + IvParameterSpec iv = new IvParameterSpec(new Hex().decode(testCase[1].getBytes())); + + byte[] cipherTExt = EncryptionHelper.encrypt(k,iv,new Hex().decode(testCase[2].getBytes())); + String cipher = new String(new Hex().encode(cipherTExt)); + + assertEquals(cipher, testCase[3]); + + assertEquals(testCase[2], new String(new Hex().encode(EncryptionHelper.decrypt(k, iv, cipherTExt)))); + + } + } + + +} \ No newline at end of file diff --git a/app/src/androidTest/java/net/bierbaumer/otp_authenticator/MainActivityTest.java b/app/src/androidTest/java/net/bierbaumer/otp_authenticator/MainActivityTest.java new file mode 100644 index 00000000..ef66b92a --- /dev/null +++ b/app/src/androidTest/java/net/bierbaumer/otp_authenticator/MainActivityTest.java @@ -0,0 +1,483 @@ +package net.bierbaumer.otp_authenticator; + + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Intent; +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.action.GeneralLocation; +import android.support.test.espresso.action.GeneralSwipeAction; +import android.support.test.espresso.action.Press; +import android.support.test.espresso.action.Swipe; +import android.support.test.espresso.action.ViewActions; +import android.support.test.espresso.intent.Intents; +import android.support.test.uiautomator.UiCollection; +import android.support.test.uiautomator.UiDevice; +import android.support.test.uiautomator.UiObject; +import android.support.test.uiautomator.UiObjectNotFoundException; +import android.support.test.uiautomator.UiSelector; +import android.support.v7.widget.ActionBarContextView; +import android.test.ActivityInstrumentationTestCase2; +import android.test.ViewAsserts; +import android.util.Log; +import android.view.View; +import android.widget.ListView; + + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.binary.Base32; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +import org.hamcrest.Matchers; +import org.hamcrest.TypeSafeMatcher; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; + +import java.util.ArrayList; + +import static android.support.test.espresso.Espresso.onData; +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; +import static android.support.test.espresso.action.ViewActions.actionWithAssertions; +import static android.support.test.espresso.action.ViewActions.clearText; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.longClick; +import static android.support.test.espresso.action.ViewActions.pressBack; +import static android.support.test.espresso.action.ViewActions.swipeDown; +import static android.support.test.espresso.action.ViewActions.typeText; +import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.intent.Intents.intended; +import static android.support.test.espresso.intent.Intents.intending; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasExtra; +import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withClassName; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anything; +import static org.hamcrest.Matchers.is; + +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class MainActivityTest extends ActivityInstrumentationTestCase2 { + private MainActivity mActivity; + + public MainActivityTest() { + super(MainActivity.class); + } + + @Before + public void setUp() throws Exception { + super.setUp(); + injectInstrumentation(InstrumentationRegistry.getInstrumentation()); + mActivity = getActivity(); + } + + public void testStart(){ + ViewAsserts.assertOnScreen(mActivity.getWindow().getDecorView(), mActivity.findViewById(R.id.listView)); + } + + + //TODO. switch to toolbar + public void test000About() throws InterruptedException { + openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getContext()); + + onView(allOf(withText("About"), isDisplayed())).perform(click()); + + Thread.sleep(1000); + onView(withId(R.id.webViewAbout)).check(matches(isDisplayed())); + onView(withId(R.id.webViewAbout)).perform(pressBack()); + + onView(withId(R.id.webViewAbout)).check(doesNotExist()); + + } + + public void test000EmptyStart() throws InterruptedException { + + onView(withText("No Account has been added yet")).check(matches(isDisplayed())); + onView(withText("Add")).check(matches(isDisplayed())); + + Intents.init(); + + String qr = "XXX" ; + + // Build a result to return from the ZXING app + Intent resultData = new Intent(); + resultData.putExtra(com.google.zxing.client.android.Intents.Scan.RESULT, qr); + Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData); + + // Stub out the Camera. When an intent is sent to the Camera, this tells Espresso to respond + // with the ActivityResult we just created + intending(hasAction("com.google.zxing.client.android.SCAN")).respondWith(result); + + + onView(withText("Add")).check(matches(isDisplayed())); + + onView(withText("Add")).perform(click()); + + // We can also validate that an intent resolving to the "camera" activity has been sent out by our app + intended(hasAction("com.google.zxing.client.android.SCAN")); + + + Intents.release(); + } + + public void test001InvalidQRCode() throws InterruptedException { + Intents.init(); + + String qr ="invalid qr code"; + + // Build a result to return from the ZXING app + Intent resultData = new Intent(); + resultData.putExtra(com.google.zxing.client.android.Intents.Scan.RESULT, qr); + Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData); + + // Stub out the Camera. When an intent is sent to the Camera, this tells Espresso to respond + // with the ActivityResult we just created + intending(hasAction("com.google.zxing.client.android.SCAN")).respondWith(result); + + // Now that we have the stub in place, click on the button in our app that launches into the Camera + onView(withId(R.id.action_scan)).perform(click()); + + // We can also validate that an intent resolving to the "camera" activity has been sent out by our app + intended(hasAction("com.google.zxing.client.android.SCAN")); + + onView(withText("Invalid QR Code")).check(matches(isDisplayed())); + + + Thread.sleep(5000); + onView(withText("No Account has been added yet")).check(matches(isDisplayed())); + + Intents.release(); + } + + public void test002NocodeScanned() throws InterruptedException { + Intents.init(); + + // Build a result to return from the ZXING app + Intent resultData = new Intent(); + + Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_CANCELED, resultData); + + // Stub out the Camera. When an intent is sent to the Camera, this tells Espresso to respond + // with the ActivityResult we just created + intending(hasAction("com.google.zxing.client.android.SCAN")).respondWith(result); + + // Now that we have the stub in place, click on the button in our app that launches into the Camera + onView(withId(R.id.action_scan)).perform(click()); + + // We can also validate that an intent resolving to the "camera" activity has been sent out by our app + intended(hasAction("com.google.zxing.client.android.SCAN")); + + onView(withText("No Account has been added yet")).check(matches(isDisplayed())); + + Intents.release(); + } + + + String[][] codes = new String [][]{ + new String[]{"WOW", "Sicherheit00000"}, + new String[]{"SUCH", "Sicherheit00001"}, + new String[]{"APP", "Sicherheit00002"}, + }; + + + public void test003AddCodes() throws InterruptedException { + for(String[] code: codes){ + Intents.init(); + + String qr = "otpauth://totp/"+code[0] +"?secret="+new String(new Base32().encode(code[1].getBytes())) ; + + // Build a result to return from the ZXING app + Intent resultData = new Intent(); + resultData.putExtra(com.google.zxing.client.android.Intents.Scan.RESULT, qr); + Instrumentation.ActivityResult result = new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData); + + // Stub out the Camera. When an intent is sent to the Camera, this tells Espresso to respond + // with the ActivityResult we just created + intending(hasAction("com.google.zxing.client.android.SCAN")).respondWith(result); + + // Now that we have the stub in place, click on the button in our app that launches into the Camera + onView(withId(R.id.action_scan)).perform(click()); + + // We can also validate that an intent resolving to the "camera" activity has been sent out by our app + intended(hasAction("com.google.zxing.client.android.SCAN")); + + onView(withText("Account added")).check(matches(isDisplayed())); + + Intents.release(); + } + + Thread.sleep(500); + + + for(int i = 0; i < codes.length; i++){ + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(i) + .onChildView(withId(R.id.textViewLabel)) + .check(matches(withText(codes[i][0]))); + + String otp = TOTPHelper.generate(codes[i][1].getBytes()); + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(i) + .onChildView(withId(R.id.textViewOTP)) + .check(matches(withText(otp))); + } + } + + public void test003CodesChange() throws InterruptedException { + ArrayList oldCodes = new ArrayList<>(); + + for(int i = 0; i < codes.length; i++){ + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(i) + .onChildView(withId(R.id.textViewLabel)) + .check(matches(withText(codes[i][0]))); + + String otp = TOTPHelper.generate(codes[i][1].getBytes()); + oldCodes.add(otp); + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(i) + .onChildView(withId(R.id.textViewOTP)) + .check(matches(withText(otp))); + } + + Thread.sleep(30*1000); + + for(int i = 0; i < codes.length; i++){ + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(i) + .onChildView(withId(R.id.textViewLabel)) + .check(matches(withText(codes[i][0]))); + + String otp = TOTPHelper.generate(codes[i][1].getBytes()); + assertTrue(!oldCodes.get(i).equals(otp)); + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(i) + .onChildView(withId(R.id.textViewOTP)) + .check(matches(withText(otp))); + } + + + } + + public void test004Rearrange() throws InterruptedException, UiObjectNotFoundException { + UiDevice mDevice = UiDevice.getInstance(getInstrumentation()); + + + UiObject start = mDevice.findObject(new UiSelector().textContains(codes[0][0])); + UiObject end = mDevice.findObject(new UiSelector().textContains(codes[1][0])); + + start.dragTo(start, 10); + start.dragTo(end, 10); + + mDevice.pressBack(); + + Thread.sleep(2000); + + String t = codes[0][0]; + codes[0][0] = codes[1][0]; + codes[1][0] = t; + + for(int i = 0; i < codes.length; i++){ + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(i) + .onChildView(withId(R.id.textViewLabel)) + .check(matches(withText(codes[i][0]))); + } + + start = mDevice.findObject(new UiSelector().textContains(codes[0][0])); + end = mDevice.findObject(new UiSelector().textContains(codes[1][0])); + + start.dragTo(start, 10); + start.dragTo(end, 10); + + mDevice.pressBack(); + + Thread.sleep(2000); + + t = codes[0][0]; + codes[0][0] = codes[1][0]; + codes[1][0] = t; + + for(int i = 0; i < codes.length; i++){ + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(i) + .onChildView(withId(R.id.textViewLabel)) + .check(matches(withText(codes[i][0]))); + } + } + + public void test005EditMode() throws InterruptedException { + + onView(withId(R.id.action_edit)).check(doesNotExist()); + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(0) + .perform(longClick()); + + onView(withId(R.id.action_edit)).check(matches(isDisplayed())); + onView(withId(R.id.action_delete)).check(matches(isDisplayed())); + ActionBarContextView.class.getCanonicalName(); + onView(allOf(isDescendantOfA(withClassName(Matchers.containsString("ActionBarContextView"))), withText(codes[0][0]))).check(matches(isDisplayed())); + + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(1) + .perform(longClick()); + + + + onView(withId(R.id.action_edit)).check(matches(isDisplayed())); + onView(withId(R.id.action_delete)).check(matches(isDisplayed())); + onView(allOf(isDescendantOfA(withClassName(Matchers.containsString("ActionBarContextView"))), withText(codes[1][0]))).check(matches(isDisplayed())); + + + onView(withId(R.id.listView)).perform(pressBack()); + + onView(withId(R.id.action_edit)).check(doesNotExist()); + } + + public void test005RenameCancel(){ + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(1) + .perform(longClick()); + + onView(withId(R.id.action_edit)).check(matches(isDisplayed())); + + onView(withId(R.id.action_edit)).perform(click()); + + onView(withText(codes[1][0])).perform(click()).perform(typeText(" VERY TEST")); + + onView(withText("Cancel")).perform(click()); + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(1) + .onChildView(withId(R.id.textViewLabel)) + .check(matches(withText(codes[1][0]))); + + } + + public void test006Rename(){ + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(1) + .perform(longClick()); + + onView(withId(R.id.action_edit)).check(matches(isDisplayed())); + + onView(withId(R.id.action_edit)).perform(click()); + + onView(withText(codes[1][0])).perform(click()).perform(typeText(" VERY TEST")); + + onView(withText("Save")).perform(click()); + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(1) + .onChildView(withId(R.id.textViewLabel)) + .check(matches(withText(codes[1][0] + " VERY TEST"))); + + } + + + public void test007DeleteCancel() throws InterruptedException, EncoderException { + + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(0) + .perform(longClick()); + + onView(withId(R.id.action_delete)).check(matches(isDisplayed())); + onView(withId(R.id.action_delete)).perform(click()); + + onView(withText("Remove")).check(matches(isDisplayed())); + onView(withText("Cancel")).check(matches(isDisplayed())); + + onView(withText("Cancel")).perform(click()); + + onView(withText("Remove")).check(doesNotExist()); + + onView(withId(R.id.listView)).check(matches(withListSize(codes.length))); + + } + + public void test008Delete() throws InterruptedException, EncoderException { + + + + + // remove test + for(int i = codes.length; i > 0; i--){ + + onData(anything()).inAdapterView(withId(R.id.listView)) + .atPosition(0) + .perform(longClick()); + + onView(withId(R.id.action_delete)).check(matches(isDisplayed())); + onView(withId(R.id.action_delete)).perform(click()); + + onView(withText("Remove")).check(matches(isDisplayed())); + + + + onView(withText("Remove")).perform(click()); + onView(withId(R.id.listView)).check(matches(withListSize(i - 1))); + + if(i > 1){ + onView(withText("Account removed")).check(matches(isDisplayed())); + } + else { + onView(withText(R.string.no_accounts)).check(matches(isDisplayed())); + } + + } + + + } + + public static Matcher withListSize (final int size) { + return new TypeSafeMatcher () { + @Override public boolean matchesSafely (final View view) { + return ((ListView) view).getChildCount () == size; + } + + @Override public void describeTo (final Description description) { + description.appendText ("ListView should have " + size + " items"); + } + }; + } + + + public static Matcher withResourceName(String resourceName) { + return withResourceName(is(resourceName)); + } + + public static Matcher withResourceName(final Matcher resourceNameMatcher) { + return new TypeSafeMatcher() { + @Override + public void describeTo(Description description) { + description.appendText("with resource name: "); + resourceNameMatcher.describeTo(description); + } + + @Override + public boolean matchesSafely(View view) { + int id = view.getId(); + return id != View.NO_ID && id != 0 && view.getResources() != null + && resourceNameMatcher.matches(view.getResources().getResourceName(id)); + } + }; + } + + + +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..ac90f201 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..2136ccd4a8ce950784064d34765e77096dae4e75 GIT binary patch literal 28962 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 D224v@ literal 0 HcmV?d00001 diff --git a/app/src/main/java/net/bierbaumer/otp_authenticator/CaptureActivityAnyOrientation.java b/app/src/main/java/net/bierbaumer/otp_authenticator/CaptureActivityAnyOrientation.java new file mode 100644 index 00000000..b1b9b1cc --- /dev/null +++ b/app/src/main/java/net/bierbaumer/otp_authenticator/CaptureActivityAnyOrientation.java @@ -0,0 +1,7 @@ +package net.bierbaumer.otp_authenticator; + +import com.journeyapps.barcodescanner.CaptureActivity; + +public class CaptureActivityAnyOrientation extends CaptureActivity { + +} diff --git a/app/src/main/java/net/bierbaumer/otp_authenticator/EncryptionHelper.java b/app/src/main/java/net/bierbaumer/otp_authenticator/EncryptionHelper.java new file mode 100644 index 00000000..acd5aa59 --- /dev/null +++ b/app/src/main/java/net/bierbaumer/otp_authenticator/EncryptionHelper.java @@ -0,0 +1,96 @@ +package net.bierbaumer.otp_authenticator; + +import android.content.Context; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import static net.bierbaumer.otp_authenticator.Utils.readFully; +import static net.bierbaumer.otp_authenticator.Utils.writeFully; + +public class EncryptionHelper { + public final static int KEY_LENGTH = 16; + public final static int IV_LENGTH = 12; + + public final static String ALGORITHM = "AES/GCM/NoPadding"; + + public static byte[] encrypt(SecretKey secretKey, IvParameterSpec iv, byte[] plainText) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); + + return cipher.doFinal(plainText); + } + + public static byte[] decrypt(SecretKey secretKey, IvParameterSpec iv, byte[] cipherText) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { + Cipher cipher = Cipher.getInstance(ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, secretKey, iv); + + return cipher.doFinal(cipherText); + } + + + public static byte[] encrypt(SecretKey secretKey, byte[] plaintext) throws NoSuchPaddingException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, UnsupportedEncodingException, InvalidAlgorithmParameterException { + final byte[] iv = new byte[IV_LENGTH]; + new SecureRandom().nextBytes(iv); + + byte[] cipherText = encrypt(secretKey, new IvParameterSpec(iv), plaintext); + + + byte[] combined = new byte[iv.length + cipherText.length]; + System.arraycopy(iv, 0, combined, 0, iv.length); + System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length); + + return combined; + } + + public static byte[] decrypt(SecretKey secretKey, byte[] cipherText) throws NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException { + byte[] iv = Arrays.copyOfRange(cipherText, 0, IV_LENGTH); + byte[] cipher = Arrays.copyOfRange(cipherText, IV_LENGTH,cipherText.length); + + return decrypt(secretKey,new IvParameterSpec(iv), cipher ); + } + + /** + * Load our symmetric secret key. + * The symmetric secret key is stored securely on disk by wrapping + * it with a public/private key pair, possibly backed by hardware. + */ + public static SecretKey loadOrGenerateKeys(Context context, File keyFile) + throws GeneralSecurityException, IOException { + final SecretKeyWrapper wrapper = new SecretKeyWrapper(context, "settings"); + + // Generate secret key if none exists + if (!keyFile.exists()) { + final byte[] raw = new byte[KEY_LENGTH]; + new SecureRandom().nextBytes(raw); + + final SecretKey key = new SecretKeySpec(raw, "AES"); + final byte[] wrapped = wrapper.wrap(key); + + + writeFully(keyFile, wrapped); + } + + // Even if we just generated the key, always read it back to ensure we + // can read it successfully. + final byte[] wrapped = readFully(keyFile); + final SecretKey key = wrapper.unwrap(wrapped); + + return key; + } +} diff --git a/app/src/main/java/net/bierbaumer/otp_authenticator/EntriesAdapter.java b/app/src/main/java/net/bierbaumer/otp_authenticator/EntriesAdapter.java new file mode 100644 index 00000000..84309cb9 --- /dev/null +++ b/app/src/main/java/net/bierbaumer/otp_authenticator/EntriesAdapter.java @@ -0,0 +1,134 @@ +package net.bierbaumer.otp_authenticator; + +import android.content.ClipData; +import android.graphics.Color; +import android.view.DragEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import java.util.List; + + +public class EntriesAdapter extends BaseAdapter { + private List entries; + private Entry currentSelection; + + + @Override + public int getCount() { + return getEntries().size(); + } + + @Override + public Entry getItem(int i) { + return getEntries().get(i); + } + + @Override + public long getItemId(int i) { + return i; + } + + @Override + public View getView(final int position, View convertView, final ViewGroup parent) { + + View v = convertView; + + if (v == null) { + final LayoutInflater vi; + vi = LayoutInflater.from(parent.getContext()); + v = vi.inflate(R.layout.row, null); + } + + v.setBackgroundColor(Color.TRANSPARENT); + + if(getEntries().get(position) == getCurrentSelection()){ + v.setBackgroundColor(parent.getResources().getColor(R.color.primary_light)); + } + + + + final TextView tt1 = (TextView) v.findViewById(R.id.textViewLabel); + tt1.setText(getItem(position).getLabel()); + + TextView tt2 = (TextView) v.findViewById(R.id.textViewOTP); + v.setTag(position); + tt2.setText(getItem(position).getCurrentOTP()); + + + v.setOnDragListener(new View.OnDragListener() { + + @Override + public boolean onDrag(View v, DragEvent event) { + + final int action = event.getAction(); + switch (action) { + case DragEvent.ACTION_DRAG_STARTED: + break; + + case DragEvent.ACTION_DRAG_EXITED: + break; + + case DragEvent.ACTION_DRAG_ENTERED: + break; + + case DragEvent.ACTION_DROP: { + int from = Integer.parseInt(event.getClipData().getDescription().getLabel()+""); + int to = (Integer) v.getTag(); + Entry e = getEntries().remove(from); + getEntries().add(to, e); + notifyDataSetChanged(); + + return true; + } + + case DragEvent.ACTION_DRAG_ENDED: { + return true; + } + + default: + break; + } + return true; + } + }); + v.setOnTouchListener(new View.OnTouchListener() { + + @Override + public boolean onTouch(View v, MotionEvent arg1) { + + if (getCurrentSelection() != getEntries().get(position)) { + return false; + } + + ClipData data = ClipData.newPlainText(v.getTag() + "", ""); + View.DragShadowBuilder shadow = new View.DragShadowBuilder(v); + v.startDrag(data, shadow, null, 0); + + return false; + } + }); + + return v; + } + + public List getEntries() { + return entries; + } + + public void setEntries(List entries) { + this.entries = entries; + } + + public Entry getCurrentSelection() { + return currentSelection; + } + + public void setCurrentSelection(Entry currentSelection) { + this.currentSelection = currentSelection; + } +} \ No newline at end of file diff --git a/app/src/main/java/net/bierbaumer/otp_authenticator/Entry.java b/app/src/main/java/net/bierbaumer/otp_authenticator/Entry.java new file mode 100644 index 00000000..a9da4dee --- /dev/null +++ b/app/src/main/java/net/bierbaumer/otp_authenticator/Entry.java @@ -0,0 +1,105 @@ +package net.bierbaumer.otp_authenticator; + +import android.net.Uri; + +import org.apache.commons.codec.binary.Base32; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.URL; +import java.util.Arrays; + +public class Entry { + public static final String JSON_SECRET = "secret"; + public static final String JSON_LABEL = "label"; + + private byte[] secret; + private String label; + private String currentOTP; + + public Entry (){ + + } + + public Entry(String contents) throws Exception { + contents = contents.replaceFirst("otpauth", "http"); + Uri uri = Uri.parse(contents); + URL url = new URL(contents); + + if(!url.getProtocol().equals("http")){ + throw new Exception("Invalid Protocol"); + } + + if(!url.getHost().equals("totp")){ + throw new Exception(); + } + + String secret = uri.getQueryParameter("secret"); + String label = uri.getPath().substring(1); + + String issuer = uri.getQueryParameter("issuer"); + + if(issuer != null){ + label = issuer +" - "+label; + } + + this.label = label; + this.secret = new Base32().decode(secret.toUpperCase()); + } + + public Entry (JSONObject jsonObj ) throws JSONException { + this.setSecret(new Base32().decode(jsonObj.getString(JSON_SECRET))); + this.setLabel(jsonObj.getString(JSON_LABEL)); + } + + public JSONObject toJSON() throws JSONException { + JSONObject jsonObj = new JSONObject(); + jsonObj.put(JSON_SECRET, new String(new Base32().encode(getSecret()))); + jsonObj.put(JSON_LABEL, getLabel()); + + return jsonObj; + } + + public byte[] getSecret() { + return secret; + } + + public void setSecret(byte[] secret) { + this.secret = secret; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getCurrentOTP() { + return currentOTP; + } + + public void setCurrentOTP(String currentOTP) { + this.currentOTP = currentOTP; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Entry entry = (Entry) o; + + if (!Arrays.equals(secret, entry.secret)) return false; + return !(label != null ? !label.equals(entry.label) : entry.label != null); + + } + + @Override + public int hashCode() { + int result = secret != null ? Arrays.hashCode(secret) : 0; + result = 31 * result + (label != null ? label.hashCode() : 0); + return result; + } +} diff --git a/app/src/main/java/net/bierbaumer/otp_authenticator/MainActivity.java b/app/src/main/java/net/bierbaumer/otp_authenticator/MainActivity.java new file mode 100644 index 00000000..2df4cbca --- /dev/null +++ b/app/src/main/java/net/bierbaumer/otp_authenticator/MainActivity.java @@ -0,0 +1,281 @@ +package net.bierbaumer.otp_authenticator; + +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.design.widget.Snackbar; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.ActionMode; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.view.animation.LinearInterpolator; +import android.webkit.WebView; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.ProgressBar; + +import com.google.zxing.client.android.Intents; +import com.google.zxing.integration.android.IntentIntegrator; + +import java.util.ArrayList; + +public class MainActivity extends AppCompatActivity implements ActionMode.Callback { + + private ArrayList entries; + private EntriesAdapter adapter; + + private Entry nextSelection = null; + private void showNoAccount(){ + Snackbar noAccountSnackbar = Snackbar.make(findViewById(R.id.listView), R.string.no_accounts, Snackbar.LENGTH_INDEFINITE) + .setAction("Add", new View.OnClickListener() { + @Override + public void onClick(View view) { + scanQRCode(); + } + }); + noAccountSnackbar.show(); + } + + private void scanQRCode(){ + new IntentIntegrator(MainActivity.this) + .setCaptureActivity(CaptureActivityAnyOrientation.class) + .setOrientationLocked(false) + .initiateScan(); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTitle(R.string.app_name); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); + setContentView(R.layout.activity_main); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + final ListView listView = (ListView) findViewById(R.id.listView); + final ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar); + + entries = SettingsHelper.load(this); + + adapter = new EntriesAdapter(); + adapter.setEntries(entries); + + listView.setAdapter(adapter); + + listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView adapterView, View view, int i, long l) { + nextSelection = entries.get(i); + startActionMode(MainActivity.this); + + return true; + } + }); + + if(entries.isEmpty()){ + showNoAccount(); + } + + int progress = ((int) (System.currentTimeMillis() / 1000) % 30); + progressBar.setProgress((int) (1000.0 * progress / 30.0)); + + ObjectAnimator animation = ObjectAnimator.ofInt(progressBar, "progress", 1000); + animation.setDuration((30 - progress) * 1000); + animation.setInterpolator(new LinearInterpolator()); + animation.start(); + + + final Handler handler = new Handler(); + final Runnable handlerTask = new Runnable() + { + @Override + public void run() { + int progress = (int) (System.currentTimeMillis() / 1000) % 30 ; + + if(progress == 0){ + progressBar.setProgress(progress); + + ObjectAnimator animation = ObjectAnimator.ofInt(progressBar, "progress", 1000); + animation.setDuration(30 * 1000); + animation.setInterpolator(new LinearInterpolator()); + animation.start(); + } + + for(int i =0;i< adapter.getCount();i++){ + if(progress == 0 || adapter.getItem(i).getCurrentOTP() == null){ + adapter.getItem(i).setCurrentOTP(TOTPHelper.generate(adapter.getItem(i).getSecret())); + } + } + adapter.notifyDataSetChanged(); + + handler.postDelayed(this, 1000); + } + }; + handlerTask.run(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent intent) { + super.onActivityResult(requestCode, resultCode, intent); + + if (requestCode == IntentIntegrator.REQUEST_CODE && resultCode == Activity.RESULT_OK) { + try { + Entry e = new Entry(intent.getStringExtra(Intents.Scan.RESULT)); + e.setCurrentOTP(TOTPHelper.generate(e.getSecret())); + entries.add(e); + SettingsHelper.store(this, entries); + + adapter.notifyDataSetChanged(); + + Snackbar.make(findViewById(R.id.listView), "Account added", Snackbar.LENGTH_LONG).show(); + } catch (Exception e) { + Snackbar.make(findViewById(R.id.listView), "Invalid QR Code", Snackbar.LENGTH_LONG).setCallback(new Snackbar.Callback() { + @Override + public void onDismissed(Snackbar snackbar, int event) { + super.onDismissed(snackbar, event); + + if(entries.isEmpty()){ + showNoAccount(); + } + } + }).show(); + + return; + } + } + + if(entries.isEmpty()){ + showNoAccount(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.action_scan) { + scanQRCode(); + + return true; + } else if(id == R.id.action_about){ + WebView view = (WebView) LayoutInflater.from(this).inflate(R.layout.dialog_about, null); + view.loadUrl("file:///android_res/raw/about.html"); + new AlertDialog.Builder(this).setView(view).show(); + + return true; + } + + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { + MenuInflater inflater = actionMode.getMenuInflater(); + inflater.inflate(R.menu.menu_edit, menu); + + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { + adapter.setCurrentSelection(nextSelection); + adapter.notifyDataSetChanged(); + actionMode.setTitle(adapter.getCurrentSelection().getLabel()); + + return true; + } + + @Override + public boolean onActionItemClicked(final ActionMode actionMode, MenuItem menuItem) { + int id = menuItem.getItemId(); + + if (id == R.id.action_delete) { + AlertDialog.Builder alert = new AlertDialog.Builder(this); + alert.setTitle("Remove " + adapter.getCurrentSelection().getLabel() + "?"); + alert.setMessage("Are you sure you want do remove this account?"); + + alert.setPositiveButton("Remove", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + entries.remove(adapter.getCurrentSelection()); + + Snackbar.make(findViewById(R.id.listView), "Account removed", Snackbar.LENGTH_LONG).setCallback(new Snackbar.Callback() { + @Override + public void onDismissed(Snackbar snackbar, int event) { + super.onDismissed(snackbar, event); + + if (entries.isEmpty()) { + showNoAccount(); + } + } + }).show(); + + actionMode.finish(); + } + }); + + alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + dialog.cancel(); + actionMode.finish(); + } + }); + + alert.show(); + + return true; + } + else if (id == R.id.action_edit) { + AlertDialog.Builder alert = new AlertDialog.Builder(this); + alert.setTitle("Rename"); + + final EditText input = new EditText(this); + input.setText(adapter.getCurrentSelection().getLabel()); + alert.setView(input); + + alert.setPositiveButton("Save", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + adapter.getCurrentSelection().setLabel(input.getEditableText().toString()); + actionMode.finish(); + } + }); + + alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + dialog.cancel(); + actionMode.finish(); + } + }); + + alert.show(); + + return true; + } + + return false; + } + + @Override + public void onDestroyActionMode(ActionMode actionMode) { + adapter.setCurrentSelection(null); + adapter.notifyDataSetChanged(); + + SettingsHelper.store(this, entries); + } +} \ No newline at end of file diff --git a/app/src/main/java/net/bierbaumer/otp_authenticator/SecretKeyWrapper.java b/app/src/main/java/net/bierbaumer/otp_authenticator/SecretKeyWrapper.java new file mode 100644 index 00000000..0a322c4d --- /dev/null +++ b/app/src/main/java/net/bierbaumer/otp_authenticator/SecretKeyWrapper.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 net.bierbaumer.otp_authenticator; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.security.KeyPairGeneratorSpec; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.util.Calendar; +import java.util.GregorianCalendar; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.security.auth.x500.X500Principal; + +/** + * Wraps {@link SecretKey} instances using a public/private key pair stored in + * the platform {@link KeyStore}. This allows us to protect symmetric keys with + * hardware-backed crypto, if provided by the device. + *

+ * See key wrapping for more + * details. + *

+ * Not inherently thread safe. + */ +public class SecretKeyWrapper { + private final Cipher mCipher; + private final KeyPair mPair; + + /** + * Create a wrapper using the public/private key pair with the given alias. + * If no pair with that alias exists, it will be generated. + */ + public SecretKeyWrapper(Context context, String alias) + throws GeneralSecurityException, IOException { + mCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + + final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + if (!keyStore.containsAlias(alias)) { + generateKeyPair(context, alias); + } + + // Even if we just generated the key, always read it back to ensure we + // can read it successfully. + final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry( + alias, null); + mPair = new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey()); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) + private static void generateKeyPair(Context context, String alias) + throws GeneralSecurityException { + final Calendar start = new GregorianCalendar(); + final Calendar end = new GregorianCalendar(); + end.add(Calendar.YEAR, 100); + + final KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context) + .setAlias(alias) + .setSubject(new X500Principal("CN=" + alias)) + .setSerialNumber(BigInteger.ONE) + .setStartDate(start.getTime()) + .setEndDate(end.getTime()) + .build(); + + final KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore"); + + gen.initialize(spec); + gen.generateKeyPair(); + } + + /** + * Wrap a {@link SecretKey} using the public key assigned to this wrapper. + * Use {@link #unwrap(byte[])} to later recover the original + * {@link SecretKey}. + * + * @return a wrapped version of the given {@link SecretKey} that can be + * safely stored on untrusted storage. + */ + public byte[] wrap(SecretKey key) throws GeneralSecurityException { + mCipher.init(Cipher.WRAP_MODE, mPair.getPublic()); + return mCipher.wrap(key); + } + + /** + * Unwrap a {@link SecretKey} using the private key assigned to this + * wrapper. + * + * @param blob a wrapped {@link SecretKey} as previously returned by + * {@link #wrap(SecretKey)}. + */ + public SecretKey unwrap(byte[] blob) throws GeneralSecurityException { + mCipher.init(Cipher.UNWRAP_MODE, mPair.getPrivate()); + + return (SecretKey) mCipher.unwrap(blob, "AES", Cipher.SECRET_KEY); + } +} \ No newline at end of file diff --git a/app/src/main/java/net/bierbaumer/otp_authenticator/SettingsHelper.java b/app/src/main/java/net/bierbaumer/otp_authenticator/SettingsHelper.java new file mode 100644 index 00000000..61ce8aab --- /dev/null +++ b/app/src/main/java/net/bierbaumer/otp_authenticator/SettingsHelper.java @@ -0,0 +1,68 @@ +package net.bierbaumer.otp_authenticator; + +import android.content.Context; +import android.os.Build; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.io.File; +import java.util.ArrayList; + +import javax.crypto.SecretKey; + +import static net.bierbaumer.otp_authenticator.Utils.readFully; +import static net.bierbaumer.otp_authenticator.Utils.writeFully; + +public class SettingsHelper { + public static final String KEY_FILE = "otp.key"; + public static final String SETTINGS_FILE = "secrets.dat"; + + public static void store(Context context, ArrayList entries){ + JSONArray a = new JSONArray(); + + for(Entry e: entries){ + try { + a.put(e.toJSON()); + } catch (JSONException e1) { + } + } + + try { + byte[] data = a.toString().getBytes(); + + int currentApiVersion = android.os.Build.VERSION.SDK_INT; + if (currentApiVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + SecretKey key = EncryptionHelper.loadOrGenerateKeys(context, new File(context.getFilesDir() + "/" + KEY_FILE)); + data = EncryptionHelper.encrypt(key,data); + } + + writeFully(new File(context.getFilesDir() + "/" + SETTINGS_FILE), data); + + } catch (Exception e) { + } + + } + + public static ArrayList load(Context context){ + ArrayList entries = new ArrayList<>(); + + try { + byte[] data = readFully(new File(context.getFilesDir() + "/" + SETTINGS_FILE)); + + int currentApiVersion = android.os.Build.VERSION.SDK_INT; + if (currentApiVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + SecretKey key = EncryptionHelper.loadOrGenerateKeys(context, new File(context.getFilesDir() + "/" + KEY_FILE)); + data = EncryptionHelper.decrypt(key, data); + } + JSONArray a = new JSONArray(new String(data)); + + for(int i=0;i< a.length(); i++){ + entries.add(new Entry(a.getJSONObject(i) )); + } + } + catch (Exception e) { + } + return entries; + } +} \ No newline at end of file diff --git a/app/src/main/java/net/bierbaumer/otp_authenticator/TOTPHelper.java b/app/src/main/java/net/bierbaumer/otp_authenticator/TOTPHelper.java new file mode 100644 index 00000000..ced4b8d8 --- /dev/null +++ b/app/src/main/java/net/bierbaumer/otp_authenticator/TOTPHelper.java @@ -0,0 +1,52 @@ +package net.bierbaumer.otp_authenticator; + +import org.apache.commons.codec.binary.Base32; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + + +public class TOTPHelper { + public static final String SHA1 = "HmacSHA1"; + + public static String generate(byte[] secret) { + return String.format("%06d", generate(secret, System.currentTimeMillis() / 1000, 6)); + } + + public static int generate(byte[] key, long t, int digits) + { + int r = 0; + try { + t /= 30; + byte[] data = new byte[8]; + long value = t; + for (int i = 8; i-- > 0; value >>>= 8) { + data[i] = (byte) value; + } + + SecretKeySpec signKey = new SecretKeySpec(key, SHA1); + Mac mac = Mac.getInstance(SHA1); + mac.init(signKey); + byte[] hash = mac.doFinal(data); + + + int offset = hash[20 - 1] & 0xF; + + long truncatedHash = 0; + for (int i = 0; i < 4; ++i) { + truncatedHash <<= 8; + truncatedHash |= (hash[offset + i] & 0xFF); + } + + truncatedHash &= 0x7FFFFFFF; + truncatedHash %= Math.pow(10,digits); + + r = (int) truncatedHash; + } + + catch(Exception e){ + } + + return r; + } +} diff --git a/app/src/main/java/net/bierbaumer/otp_authenticator/Utils.java b/app/src/main/java/net/bierbaumer/otp_authenticator/Utils.java new file mode 100644 index 00000000..45c83ce3 --- /dev/null +++ b/app/src/main/java/net/bierbaumer/otp_authenticator/Utils.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 net.bierbaumer.otp_authenticator; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Utils { + public static void writeFully(File file, byte[] data) throws IOException { + final OutputStream out = new FileOutputStream(file); + try { + out.write(data); + } finally { + out.close(); + } + } + + public static byte[] readFully(File file) throws IOException { + final InputStream in = new FileInputStream(file); + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int count; + while ((count = in.read(buffer)) != -1) { + bytes.write(buffer, 0, count); + } + return bytes.toByteArray(); + } finally { + in.close(); + } + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..b5c8ee55 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml new file mode 100644 index 00000000..6a9561d6 --- /dev/null +++ b/app/src/main/res/layout/content_main.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_about.xml b/app/src/main/res/layout/dialog_about.xml new file mode 100644 index 00000000..bd0249aa --- /dev/null +++ b/app/src/main/res/layout/dialog_about.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/row.xml b/app/src/main/res/layout/row.xml new file mode 100644 index 00000000..e7d8fd98 --- /dev/null +++ b/app/src/main/res/layout/row.xml @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_edit.xml b/app/src/main/res/menu/menu_edit.xml new file mode 100644 index 00000000..2295af22 --- /dev/null +++ b/app/src/main/res/menu/menu_edit.xml @@ -0,0 +1,18 @@ +

+ + + + diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml new file mode 100644 index 00000000..a8d164c2 --- /dev/null +++ b/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cd604e410ede9289f91d42505f49a4a697d04c1c GIT binary patch literal 2923 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 M07*qoM6N<$f}${Lt^fc4 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2fc0061321306b4b2393ee903f48a0e2628a75b0 GIT binary patch literal 4158 zcmV-E5W(+>P)nJ)mS7#f-f1E}Vn2gkDW zP*P@J##%VQs;{qinFw%Y{g6jt*X|(o_P#qxwEdAq9RqS0KI1$555Jj+5WzKOdkAZx zgfpMC`xRe!_P}y211M z-weFNAHyFzq{g{$&heyM+QV%2WvJq1t-C`?w1cGtxW|c|B6gJ+C$u8%fXO!Q*sI0b z!N1|$xCX9eq#@XUix&c)-`(w1q#Z0Gz`R84f)TK<6B`y7{=QCH_lVHg_Xd7zh!kAw zC0rY2FjhXh1-__bXbAyeh)Bj61E7xrOx|xyrPUj;8{-HXLk2Cw5XxkaL^uh%c4=FG zcu~jh-;fGl7Jv?bu89b^Nrd2#T*qS2XSPBQ5P-Qpqa<;F(-l z19YQg2&Iaa?F!0@G8?LkO|*K1dhCzlj)5{#sV@Y;-=fVDG1ed$5nm7@%2_>Vouu6x zSf+~4<=Wn0Qh`IG8h=w}I4PSzo|um~gt{=_AlZa&jFau$ox{iLHy!J+)PXl(!vYe{ zqfUnEDmlF`Ztouuw)z>?crhIIwNd$O>lJ6G&&>IT1 z11~5Pig}VCfjL_wGbH^#PFwwmEoi-&zF~xKT<#?!VJlBUTi*wnwH^L5(i7MP^SALs3GdmP^$!vvSVD1bb~wz1&!SYSadY4c>-ftt?lb)4%b1! zzZs_L21nHG{ajvs=g_qF0_N=t+xxQwasG${gMTwRLU`KR%VllT=1)sxH0pHu;e~K2;Fopua`*hAf;;?ZBKOw=5!|`M0oget{taApSCgz+$J&mTwR* ze~(z;YkG9tCkMmK27bIjo~pWbNXaQ*Cs{Ia$ZdPravO_Ue^JxUCFG<(iV^j+>}uh@ zeZyBY@R^-`Er_cnawesyD8HfWa$2b-ikBifcxR6GJCU&SAu(D%TK;r^&+PNpg1O4= z-O?a|V8!=>t!TZN0IzQ zI=w*I(Dj3&sC*f)jG0PqFsl8ny#r!M4;-!`zz}Layr+Px=-kQw4qE=qLmAxFx6OCX z`|dB0MlT>4GLo~5%BEjU$rrG(3`I$ai~#?srSgMN(@?Yn@Edlv8$70Ga2xlZw_VI4 z0O#Gjy+;xe5#E*c+fU2at@Af|oQP?^s%e)>jJO|hz9FE{@+Ug()koHw-3!o*BtQV4 z7pR(da%pAt6{h+Emc2O_w)GvbH0wo%YbeQr<}=wbW*z+vW)px+S_-}Z*)ay{h%M+{uou+0kV-0mH}U@B~W58Ryk^{wqF9wP@1B zj4eJYXy`5$Lw+!}V-O$`?M#+D!4Fsx5k2`}mZW4zv8lG2I%C(#q912fYwsgsL_Zi< zGgqgNJ{e;X0VkijOES?Aa)R`#mS=#i(HSRpmP}@h_uN|TDT1wsQ5j;uyxl7u?WC4;!F|n*xp>}5J0ias%iZrbHrnYCD@(ifpWQw zF!ix;EW;AR9PUhjk3_7ww^dRzWq8l7)qdg~8tQ+`;2azMjN6zWOn!V;ECMirjyYUM zb-?4NDswfubMly^Frx${s7k}t+Wre-L_o|ifo^RA_TR6R8j}<082-#9g<3k1R()y64T;=wLt?PyPD{`gp7FVXRSqmut#7{;Hd{Qxsy+?<|k7CG%{L@YGPpRu?KSb zrH#Vl0H!UFzy{+|%4!c0jJ+t^|2x~vs5&=>s zVRUJVrsMLRYhu>}V`J+?o#==*oPCW)SDI(bc4f?pXB1IHwdP|6x)eka=9EKl5bK$Y1O!GS{#A^ z(MX{3qipEnR;Oe&1Jeohf1C_?%=hsLFKFEH>2!|^Ns$0Cd;%MONZ-erB6N9A_L8;}M`viXAh9ePR5efR%KT^=t!^P(o?*o<;Q`25lhP)RqUlm)>yz_#+ zY|MfAh~{0bqxQ>t%O)k^ha;Y5JKwCDwu=iFFHHuPCh5O0DZ?5ebcT)Pb&q2Y1TboE zn{0Fr)K5wP?(i`u_QyQ|iTS0^14D}C)b@>_jQQQMEIOf}srx&7+@5AS2pDbs)yOnhEwgW7fcHQy!Ll3ahq+?%9-dmEIm}CuG3S%RguxunG(e zl+6)wyL)G!urNH(AWWW+TBB^-F+96z-`J=pyH-2A?C!({U_`MJefLBcS#WR=B2*IJ z?t!PDC4t?Dy`5W|p8pWAp%_iee$wbp?&w~`mL(Te9YV6fho&U;cEFty`QZkP^{-Qw z90n#78;|LH(uT9YzrQz~T3eA?R(k@o{k&6lBoY8VsWa|;Oe);`2ZF^)KI+k9Gwn~< znCs)?!}|kbt2dTw{_IdBQ1H?FlhsupU_lA5`+7{Q@?;JEZc5pL1%c#!k&BB{@aLS8`-L;{l`lLNO=F!dgh~_62R6usH z%zUxH>012>OSqt`XHXEHFodVjtx8f=yrLu#0l&|X9pwqA3(IojbBp0{vE{%LT6fo( ze7qG;f=nh0q&;CpT(;_goLcQQY*bWy^O1zMD>*dl&)V8{Ehaq=q2>y{x-SC zqWl5_lM?gG-pH-%;M8nF#}?+6^*2-Kx0uoBxOI7(-tr9$#H>~nt(&jZz!#E$FwE24 z-+v)_UyAs6MbgI3jTM^Ha9;Ge1NulwN+GL0wB#ck=4eTdb0&J7qKn4(OKF`v1$)0q z_Ju^m&C}B}c>etPcrxj7cX#*rxU9nc>6MKaMk6KN;0-!{J~E>kuW#`;^edQqIFxa1 zM*gEHGs=!Spf0GBHIgi`CCSU(&o7Mjh7~_sxG*z5x8&K(%BFA8%II7(zkrFCCu{Ty zTm#pl8jtT#7L+MIyXd(Ep`qE-(JrNB_mZmQm?}wbh#1^_eBhuX(WQeHF3eb)p?o;C zyzW!@2tJd(`C#V}CQ}wm6_b{m@-o7%gMI!HR-z1dXDIgthK6QQxrszsX_Vt;3zB0} z<_!HrS0*X@5=z7hWRg*F3T=Y2^!4;D4d(y|CN)^k7()s|T754WH33OCztewzT?jU7 z=^z?;O8S<@uW(IVo0K7rw)jYtjmnLuwKH35`34t81dS*lO>zRlD&K&Bl;!aoIur6X z9Z1=<^`p${mP-UIeu5jV6*F<{d5#kqA3_oWu_hCmu59QU!TOw((rq8%+_(m=g=^y4 zCy%|LD~hY%O)J-& z%BXDooRITP_SUxVvbVK=&l~xS70CEsT1DL{MM?F$3CglVv6=aM3E=HG7tTpRkLG2D zToc!(F};T)0B_7k@b@G@bK&%`CKVGKbbK9Ptt7y$#-JbMLgwq69263k9xiBKK z0EW-_&dbklJ$^%fyP9fxG(A@Y&W&qOeV5T|&$TzHF^33es=4#24EW)OX2jl*2iilz zsbob`Kw(h=TSotf--P2B9E)?%o1^{cx#!bsIXgP=rV!#vXW;;x4^|ow$q0a61JrRA zjQ`Qq2tM=}cY2Olz%3+%k>X4xV=e_%XzY9EIbuG?YnosG2VME9d~9GK8~^|S07*qo IM6N<$f(SnQdjJ3c literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c8441026a1b2690b78c592fb2aab072fc117eb7b GIT binary patch literal 6487 zcmV-d8K~xoP)tNkl}F?Y?ZH$k)y9>Kc^`a695x#S34kDPcGdw5eqJLG z>~!}hf=gh4VF6{u_sWhO@o3OY1jIvD&6e}n(Qd*Ho>K*9h^ z1eBG5h6y!#ovI!WPz@i;QfQ`|l-ikDh4$D_mBTYHskAe{C%^v>`T4w3Gjoyre3{rh zF$!&-{u}%c-b3C?e};UP-UsZ9>=X74-?1E^)`9WFjW5u^l@F|;)J(T1HOKB&X=dIc zj{b}V3a$*k<4uKj<}UbdI0j@P5loh}G2w~*HwY~X%}gm_ij#yLeur$L&&l(IF)pLc zfe~lVgOQ)FtYobB&7slzq5rP$yB1grj^!jA8-~BdjTPp^%^Yk)YaH!t0ddwHKqRq;~nymnSAyIAhHt2o9^I(pj8e}PY^BgUrQVW03mt> zoya9vj0P18P*>Q;N*_Bp2RIkqi$wG|SHRIuXA^+jPaJST=gPi}S3hORhR1kWhnX(G zIl;M!Kw3xOum?~vJX=Ht`j@((!K<6HQk&BQre6}lU%cEnh?@~QXNfk@PD@mp>1Xu- z=ZX~rM_-s*0wnsJhi3P%wVtSt3~X%R)N5xWF;p>w%4i!(;I>37!5F|;=o=$()6Y6~ z%Ax5aWbnS!$)qS7Gj;~|C5#EfKq4D`UnJ8TP&QpN5pJkjQ)nYu+q>+rX7k#pySv+Q z5j@(NYgt#ERb*7WW^N19E6n&@t8WZ#t{_NXMT1jY$(8l04(o`(SwJ!l*E_>A$3@mzA2yO}r*C2yNU*wy z$+A?lvrZ(YKhC7)II$@LV)cbcI^~xSPCOx1tDOaz>N7%5b#T}h+bYD6#X(3@3{JgW z+0gATC~?hP4b|GQNTp`x0|d_{(edk^sa>K$q@Mgx+O6Iu2qt1qKrt|#NKWpo2p)bw zBp;eQE9*RzY}FuI3|s2)1aeY;$BMED9-cr{qn`LjMaM{rRfaPL_40wq2y!x?ir_I5 zhz7^cN?W_FE1WTDWO?714@uSE7r|p*5LFEv{a~-EJ-{kbe5Q~^4%A6;HQN%wV@43c ztE}(nI}(Y+#nOw7Oex)t9Q0!%cq|A+LsL{m_vn+vH}*QdvbfM?C)Z5hz^e0zPP7Cg zzL2XYsmjjLTQ*o?t1P4P)wLYF z7Bw(FU^AhtqEq`W@u4H{p^OP#BsJ8UD|A74dgs9y>hcR4)&v|~`{xVx)XVp!P`zVn z;fJ#RF{-$!Z-UoQ#w?gr*{umD>~kKoF5a5LAk;kh_;OH7K+Rtw*qH491 zf^F0X-&J;ve7;Z77_$Kv&0sB0`C;QxdqmuKGWyk=$M`t<&JIz5@=y|9OaTdlH_L21;$zQxzohRloH+UHP z{Am|TJJ~BlL4BpNdz8vkwp4J&LM--qng-C&)Epn0$6R4+Is5c+AFBQEsIUM;d`YuV zYyYn}VIhWnDi*X4FGWMhwoDPsP#FB|NlzMZ>z&%`+S?Drk}PIQa9XIMYlO;{x8$!o z3oT||>ZyD9VjoD=4LS1ii+0p&KTD)0XWN#f+k+GR)MIz$P#2zc;tWK0-%>8Lw0^E; zaGWYR(7V1F8B_PEXz2~*tL_6uLPNR->NGd~K(*q2)C5Q{t-+NZ>Nq?ixVq5(gPJcS zrLyRC>l?xfrG0h(*eGAE8t*(1V~}|afrl}uD4%=UjS~<7OA1U(&sFsup$cj{X2Itf z>(Db~hvuLdn))4TpE<7QwhU<)2wSG?^X^;AIROypE`hPo9JIfdwGRA&_WA)cY} z%aFq$RbS1%U;IfrCjf$QJ38GZtkuxSYgy~yM`4MHzN-#nO!zAA9?55gFHQ}b!Hk(w zSk1m)`&j~KDgFJo6$(vEFVuu&^|DomG5nANp|cMmBrDkEhX0Ed@XqPb7VNhX)BB3$+tsfqz{`hDx zrvSS6yaUxNtR{Ur8rZ^2M%!b-d0X)Cq zQ2Ve=Xf3ed^U9t>R8DncH(()ArE)S=|2S6lkDa91hljZ@V=^#Zg9-)hI&+q*Ctv0ZK)2s4Tf@ARD_!S-R2{M{E(P;) zHh!#Kcr6~FgPMBZKpkYQ0B{S@`OEhC4>B89?1fHNxst9Gaqqbb06S<;>#BT)HZqk>O~p zfOe2}YL$F8%@0lXSuJ>AdgZ|AQDL^a5+N9N7$u|;Ku%S?hsXH5=L_yt2S->V zfFL7;LZ|=%G+)(sl-j3gz7a5C6oBwe&Lji7lP>|y7~HFFjR1Pp#P$+v>r%{Db=(h_ zFbY7Zfn3N3pX3`r&z{P(N&uZYDHSGwu-U$_p%a?zU5!oU;Z3eJ>WP2n`?h6RCBxdl zbLj;;s()foxX&pguf2J-w@+zzh z28%n1ATl$64h~(6&)Nea%z4ELRR%z5h0+AFgRjhyh3RW`wX`K(Y)BJf7B=*CY-VPr z(E!@MYhR39GyQi<01z0g3YeX?$6``rOvUv=oB?VmcAG?bxwJuAcnZQUC-7 zD}$M%)6kZ~+0{yP|JjhOTjKy5SdCs8K*apygOmSlNdN+amBCK2wN$%!73TB&;jTqs2{-{qs#R$LmMhARCUj-QoRpY0U)h6h^&o? zAcVnFeI!!l3BZIY0P@VOZEvvVM^^%%;CAD_aW!2_g8R1M)*HBcqG{x)Q9aDyY+w#s z1rapW!zvwV1=2pigi!#Bj*bSU?U7kfzK5{SMO%OVsm+3jnTc;7`Yt|QQVN(r4PPCEXU{&AYUs;2Y z=BwS73r%FHDgI&xrfhx#Ye24sYCwy%pn$|gck=2l?a=&C1dwfpGswVT$=}@-b@u@l z5EDi{0>M5cBm~gryHDBLB0dA2`*3E7AOZnh-Z@Mq=a&ru77!Cg0f-UOUeP-xX@q^w zi8{!(G!dhJN?Q89*czL#7qDQAfMyG65KErKLS_9&UljplTbc;!KMVS&6_&UQM=UsQ zFkzXyjsE`ra3fhzY1_bY5kR)3iI@W7E@{m@kW9gSg2oEyMI96v7zmk|e`ZNlB`9)H z3)x6GW9mO`gL4U)`wq|oIv~Jk0nH9$po6@LMG`gsp2$KggXqo^mGoSiuKo0f#a0C( zI(!XHQF*m(9}?fkAO@UGU2L2#D0+p5`+yEAY3@BK0%&#CdgzcUZ z3-dMRp7@n?(B`yUsg&&epJEu}7eKsgA`D-pZG)F$(|6Z_?^mOL*v(~hcOrzb%AS$8 zL>A&7KzxR?#q3Mb46AW$L*F}_s(+TG^GIQ#d*#jJ(4EG|n$aSF%mX6uEff#a3Z%!u z=N_ShjZ_$Rwc)*8N+ifK? z?wgBF%7n^un1_eQn#HreXJ@#Q4H}E03lH=>4(XoALe>u;U>|zkt3vex_!fQaYV@-) zOc2vf$U@<@@c?I&6<0F||?spP%IH-Im>W}hWm z2&eeB5Q{H5(DNFEGP!obJ;lOs} zR5w0S**z+TG7AY^`#UsLR$28^;2ZSOW^;zF%=E1=$laVmDxgAH{Pu23cC#!va$6Lr1nuzrS|akl@!z;{3AR%S_Bcl;E|CFcW=#sKY950Zbn<3<)m+9}r(a=<-M3I9MWhbXWEO zffm4IW;BRu+ddLC5kG}OlEj=w@Gu!yfGJDVS%RpB1Ep^VLi8sPD$U*}Z+;j0PY^_6 zioc$zIvG>Arz^X%?maTrdl5V_m~pxDIaRTqAOf`?=r*7WBJCKWcI{WZM#gw2j5UsVAo{@3(nPbdnHiJb2RS=C!wDsj6HFy(<)hGx zLf_pXhO{+>tc8`t`l+2IRZozyO{1A-VXT96(sio=&vGCN#3I&qa-u0a3oCAgI{~T( z#zhdVVQ#Civ1=|Rx8z4KrnF#Q7~?=|2ai4wxucld>g42#2#{Fr5r{a(9OlPJ%jxq_Xy9=uv^0 zd@o(4UoUSvB)4$3%`;i-3u|Ed)dfFh2vj zC^f(A=Ay=~3)q+@3T&MwFc=dkgA~-YUr5e8La`izW9fop zYrjF@8@r>?69r$S(*g|xkkXQKijL*0+CQPw{i~UpE!B;#Fd0^Pr8nCFb`@5{NnrDKy#n$Lyogf@57oqDNQ`yKFYZv}_g~d9#xg|ro4}%zy zklMIQs(6kJ?l(Z|bni8YZR-thR~9b>5~j<$h6!-Gz9ARO^Wh08O$eN19D`UK6Mq~d zXIo_*W7)_H<9j$zwxgRH6vz- zLlG6GAmCg`sCtl`n*xE80_Tk3DgwvCOJp6>w2>QT7eLt&WH7gb24EP;#ls^zd0)}> z+12&$FsxKFIDQpOaCL*GAWk#$BqtM|XwX1*Nwdv?o^RmTcVy?^K+Yv+k#QDu&UOS& z5RQSHEo+_CvXKK)wL3OK1=IS7T$QZQmP#yROMGHOdZF~xzM97Oh+{57_6E#^`hu-7 z<~ak^!anDheAmKv(@X=PDJI7T$C#ckJrx$8&A>%~ix%%R{JG0Vr5j^Vv zq#(RHK_k%_8(JF7L~q@m(wv^V|IR%X^=}l`cYFplCCK7vKrJj&tjkh!Y;ZBA&@uM^ z==Z|&L)f3i{Q{T(Fd+D$x?WJ*{#kZq{p)Z{k@3l`UaDA}P1%zc3xpEV9SP2}I?z=g-BrapmP)1StJ(6o@h$i)>?0|s z=s4^X_U+){Ai?iS!|!I8h5^ksJl0@5b}u{^ClNUO0%;>=Bn&HgvA_g#LvZIM6ef3| z^B9KG1ywpSL?$@j-P=3gHz2soKQy%5KP0@8Hu5w44gQA!$%1gn`urkTPzW6CqgRHe3 zEdgyif+G&!vkgD975A|P_ZfuW;fvqtfyW?%#!A7o5uN6U;?xCeD(~(;~Hhu5W-H**}{|{DQAT&ejk6Hi#002ovPDHLkV1irhH|78U literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ae4cb5900c01ad0797de71b71be9e699dc4ebc8a GIT binary patch literal 9411 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 + + + + + + +
+ +

OTP Authenticator 1.0

+

© 2015 - Bruno Bierbaumer

+

Project Homepage

+

Acknowledgments

+
+ + diff --git a/app/src/main/res/raw/opensource.html b/app/src/main/res/raw/opensource.html new file mode 100644 index 00000000..ceaa0838 --- /dev/null +++ b/app/src/main/res/raw/opensource.html @@ -0,0 +1,31 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml new file mode 100644 index 00000000..51ada8a7 --- /dev/null +++ b/app/src/main/res/values-v21/styles.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/values-w820dp/dimens.xml b/app/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 00000000..63fc8164 --- /dev/null +++ b/app/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ + + + 64dp + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..50c6e04f --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,11 @@ + + +#607D8B +#455A64 +#CFD8DC +#FFC107 +#212121 +#727272 +#FFFFFF +#B6B6B6 + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..812cb7be --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + 16dp + 16dp + 16dp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..38b6b3fc --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + OTP Authenticator + Authenticator + Settings + Scan QR-Code + No Account has been added yet + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..04fa64e2 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,19 @@ + + + + + + + +