diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..f51133129 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +language: android +jdk: oraclejdk8 +sudo: true + +before_cache: + # Do not cache a few Gradle files/directories (see https://docs.travis-ci.com/user/languages/java/#Caching) + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ + +cache: + directories: + # Gradle dependencies + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + + # Android build cache (see https://developer.android.com/studio/build/build-cache.html) + - $HOME/.android/build-cache + +before_script: + - mkdir -p $ANDROID_HOME/licenses + - echo "8933bad161af4178b1185d1a37fbf41ea5269c55" > $ANDROID_HOME/licenses/android-sdk-license + - echo "d56f5187479451eabf01fb78af6dfcb131a6481e" >> $ANDROID_HOME/licenses/android-sdk-license + - mkdir -p $HOME/.android # silence sdkmanager warning + - echo 'count=0' > $HOME/.android/repositories.cfg # silence sdkmanager warning + - echo y | $ANDROID_HOME/tools/bin/sdkmanager 'tools' 'platform-tools' 'build-tools;27.0.3' > /dev/null + - echo y | $ANDROID_HOME/tools/bin/sdkmanager 'platforms;android-27' > /dev/null + - echo y | $ANDROID_HOME/tools/bin/sdkmanager 'ndk-bundle' 'cmake;3.6.4111459' 'lldb;3.1' > /dev/null + +script: + - ./gradlew clean build diff --git a/README.md b/README.md index 3131d1df8..dfe23d0c9 100644 --- a/README.md +++ b/README.md @@ -80,14 +80,12 @@ Authorizer does not require root permissions when it is allowed to write to /dev ### Compatibility -| Features | Windows | Linux (⚠️) | macOS (⚠️) | iOS | Android | -| -------------------- | :-----: | :---: | :---: | :---: | :-----: | -| AutoType - USB | ✅ | ✅ | ✅ | ✅ | ✅ | -| AutoType - Bluetooth | ✅ | ✅ | ✅ | ✅ | ✅ | -| FIDO U2F | ✅ | ✅ | ✅ | ✅ | ✅ | -| FIDO WebAuthn | ✅ | ✅ | ✅ | ✅ | ✅ | - -⚠️ For macOS and Linux, Chrome or other compatible browser is required. Firefox does not support FIDO / FIDO2 over Bluetooth natively on these platforms. +| Features | Windows | Linux | MacOS | iOS | Android | +| ----------------- | :-----: | :---: | :---: | :---: | :-----: | +| AutoType - USB | X | X | X | X | X | +| AutoType - Bluetooth | X | X | X | X | X | +| FIDO U2F | X | X | | | X | +| FIDO WebAuthn | X | X | | | X | ## Features in Detail @@ -125,6 +123,7 @@ In progress - The experience of Bluetooth-stack stability can differ between devices, as it is dependent on both the Android version and the specific device being used. - Due to limitations in the Bluetooth-stack, Authorizer can only be paired as Keyboard OR as FIDO Security key and not both. - It is important to unpair from the other device as well to prevent unexpected behavior, when establishing a new pairing under a separate profile (like Keyboard or FIDO). +- FIDO U2F & WebAuthn is currently not compatible with Apple MacOS and Apple iOS, as they expecting a different HID_REPORT_SIZE. - Currently, FIDO credentials can't be added to existing records. diff --git a/authorizer/build.gradle b/authorizer/build.gradle index 8ce88f114..78fdc0ba7 100644 --- a/authorizer/build.gradle +++ b/authorizer/build.gradle @@ -1,5 +1,3 @@ -import java.text.SimpleDateFormat - /* * Copyright (©) 2016 Jeff Harris * All rights reserved. Use of the code is allowed under the @@ -10,31 +8,17 @@ import java.text.SimpleDateFormat apply plugin: 'com.android.application' -static def gitRevision() { - def cmd = "git rev-parse --short HEAD" - return cmd.execute().text.trim() -} - -static def buildTime() { - def df = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss Z") - return df.format(new Date()) -} - android { namespace 'net.tjado.passwdsafe' - compileSdk 34 + compileSdkVersion 33 defaultConfig { applicationId 'net.tjado.passwdsafe' - minSdkVersion 21 + minSdkVersion 26 resourceConfigurations += ['de'] - targetSdkVersion 34 + targetSdkVersion 33 versionCode 500 versionName '0.5.0' - - buildConfigField 'String', 'BUILD_ID', "\"${gitRevision()}\"" - buildConfigField 'String', 'BUILD_DATE', "\"${buildTime()}\"" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" //Reference them in .xml files. @@ -52,21 +36,15 @@ android { debug { debuggable true minifyEnabled false - proguardFiles += getDefaultProguardFile( - 'proguard-android-optimize.txt') proguardFiles += 'proguard-rules.pro' proguardFiles += 'proguard-rules-debug.pro' - testProguardFiles += getDefaultProguardFile( - 'proguard-android-optimize.txt') - + testProguardFiles += 'proguard-rules-test.pro' ndk { debugSymbolLevel "FULL" } } release { minifyEnabled true - proguardFiles += getDefaultProguardFile( - 'proguard-android-optimize.txt') proguardFiles += 'proguard-rules.pro' ndk { @@ -75,8 +53,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } productFlavors { } @@ -102,33 +80,17 @@ if (project.file('../sign/sign.gradle').exists()) { } dependencies { - def room_version = "2.5.2" - def yubikey_version = "2.3.0" - def espresso_version = "3.5.1" - - api 'androidx.annotation:annotation:1.7.0' - api 'androidx.cardview:cardview:1.0.0' - api 'androidx.constraintlayout:constraintlayout:2.1.4' - api 'androidx.gridlayout:gridlayout:1.0.0' - api 'androidx.legacy:legacy-preference-v14:1.0.0' - api 'androidx.legacy:legacy-support-v4:1.0.0' - api 'androidx.lifecycle:lifecycle-viewmodel:2.6.2' - api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' - api 'androidx.preference:preference:1.2.1' - api 'com.google.android.material:material:1.9.0' - - // Avoiding duplicate class errors - implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22")) - - implementation 'androidx.appcompat:appcompat:1.7.0-alpha03' + def room_version = "2.4.3" + + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation project(':lib') + + implementation 'androidx.appcompat:appcompat:1.6.0-rc01' implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.3.1' + implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview-selection:1.1.0' - implementation "com.yubico.yubikit:yubiotp:$yubikey_version" - implementation "com.yubico.yubikit:android:$yubikey_version" - implementation 'com.github.tony19:logback-android:3.0.0' implementation 'com.github.bmelnychuk:atv:1.2.9' implementation 'com.mikepenz:iconics-core:2.8.1@aar' @@ -138,12 +100,13 @@ dependencies { implementation 'com.mikepenz:fastadapter:2.0.0@aar' implementation 'org.bouncycastle:bcprov-jdk18on:1.71.1' implementation 'org.bouncycastle:bcpg-jdk18on:1.71.1' + implementation 'org.bouncycastle:bcpkix-jdk18on:1.71.1' implementation 'io.fotoapparat.fotoapparat:library:1.4.1' implementation 'com.google.zxing:core:3.5.1' // FIDO (WebAuthn/U2F) dependencies implementation 'co.nstant.in:cbor:0.9' - implementation 'com.google.code.gson:gson:2.9.0' + implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.guava:guava:31.1-android' implementation 'rocks.xmpp:precis:1.1.0' @@ -155,25 +118,25 @@ dependencies { androidTestImplementation 'androidx.test:core:1.5.0' // AndroidJUnitRunner and JUnit Rules - androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test:runner:1.5.1' androidTestImplementation 'androidx.test:rules:1.5.0' // Assertions - androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.ext:junit:1.1.4' androidTestImplementation 'androidx.test.ext:truth:1.5.0' androidTestImplementation 'com.google.truth:truth:1.1.3' // Espresso dependencies - androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-web:$espresso_version" - androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espresso_version" + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.0' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.0' + androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.5.0' + androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.0' + androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.5.0' // The following Espresso dependency can be either "implementation" // or "androidTestImplementation", depending on whether you want the // dependency to appear on your APK's compile classpath or the test APK // classpath. - androidTestImplementation "androidx.test.espresso:espresso-idling-resource:$espresso_version" + androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.0' } diff --git a/authorizer/proguard-rules-debug.pro b/authorizer/proguard-rules-debug.pro index 3851c7c19..89bff6b5a 100644 --- a/authorizer/proguard-rules-debug.pro +++ b/authorizer/proguard-rules-debug.pro @@ -18,12 +18,3 @@ -keep,includedescriptorclasses class androidx.** { *; } -dontwarn androidx.window.** - --keepclassmembers class **.R$* { - public static ; -} - -# -# Needed for tests which use Kotlin -# --keep public class kotlin.LazyKt \ No newline at end of file diff --git a/authorizer/proguard-rules.pro b/authorizer/proguard-rules.pro index e2d17c918..e2369a6ed 100644 --- a/authorizer/proguard-rules.pro +++ b/authorizer/proguard-rules.pro @@ -20,32 +20,4 @@ -keep class net.tjado.authorizer.* -keepclasseswithmembernames,includedescriptorclasses class org.pwsafe.lib.crypto.SHA256Pws { native ; -} - -# -# logback-android. From project wiki narrowed to just what is needed for logcat -# - -# Issue #229 --keepclassmembers class ch.qos.logback.classic.pattern.* { (); } - --keep public class org.slf4j.impl.** { *; } --keep public class ch.qos.logback.classic.** { *; } --keepattributes *Annotation* --dontwarn ch.qos.logback.core.net.* - -# -# For stack traces -# --keepattributes LineNumberTable,SourceFile --renamesourcefileattribute SourceFile - -# -# Misc -# --dontwarn edu.umd.cs.findbugs.annotations.SuppressFBWarnings --dontwarn javax.annotation.Nonnull --dontwarn javax.annotation.Nullable - - -#-printconfiguration /tmp/full-r8-config.txt \ No newline at end of file +} \ No newline at end of file diff --git a/authorizer/src/main/AndroidManifest.xml b/authorizer/src/main/AndroidManifest.xml index d0061150f..da354aef2 100644 --- a/authorizer/src/main/AndroidManifest.xml +++ b/authorizer/src/main/AndroidManifest.xml @@ -41,12 +41,6 @@ - - - @@ -68,7 +62,6 @@ @@ -79,6 +72,11 @@ + + + + + diff --git a/authorizer/src/main/assets/logback.xml b/authorizer/src/main/assets/logback.xml deleted file mode 100644 index 692c1c372..000000000 --- a/authorizer/src/main/assets/logback.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - %msg - - - - - - - - \ No newline at end of file diff --git a/authorizer/src/main/java/net/tjado/authorizer/OutputInterface.java b/authorizer/src/main/java/net/tjado/authorizer/OutputInterface.java index e11b201f7..0783a2b02 100644 --- a/authorizer/src/main/java/net/tjado/authorizer/OutputInterface.java +++ b/authorizer/src/main/java/net/tjado/authorizer/OutputInterface.java @@ -10,7 +10,7 @@ package net.tjado.authorizer; public interface OutputInterface { - public enum Language { en_US, en_GB, de_DE, AppleMac_de_DE, de_CH, fr_CH, fr_FR, neo } + public enum Language { en_US, en_GB, de_DE, AppleMac_de_DE, de_CH, fr_CH, neo } public boolean setLanguage(OutputInterface.Language lang); public int sendText(String text) throws Exception; public int sendReturn() throws Exception; diff --git a/authorizer/src/main/java/net/tjado/authorizer/UsbHidKbd_fr_FR.java b/authorizer/src/main/java/net/tjado/authorizer/UsbHidKbd_fr_FR.java deleted file mode 100644 index bd953bd38..000000000 --- a/authorizer/src/main/java/net/tjado/authorizer/UsbHidKbd_fr_FR.java +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Authorizer - * - * Copyright 2016 by Tjado Mäcke - * Licensed under GNU General Public License 3.0. - * - * @license GPL-3.0 - */ - -package net.tjado.authorizer; - - -public class UsbHidKbd_fr_FR extends UsbHidKbd { - - public UsbHidKbd_fr_FR() { - - kbdVal.put(null, new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("a", new byte[] {0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("b", new byte[] {0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("c", new byte[] {0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("d", new byte[] {0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("e", new byte[] {0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("f", new byte[] {0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("g", new byte[] {0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("h", new byte[] {0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("i", new byte[] {0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("j", new byte[] {0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("k", new byte[] {0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("l", new byte[] {0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("m", new byte[] {0x00, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("n", new byte[] {0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("o", new byte[] {0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("p", new byte[] {0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("q", new byte[] {0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("r", new byte[] {0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("s", new byte[] {0x00, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("t", new byte[] {0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("u", new byte[] {0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("v", new byte[] {0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("w", new byte[] {0x00, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("x", new byte[] {0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("y", new byte[] {0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("z", new byte[] {0x00, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00} ); - - kbdVal.put("A", new byte[] {0x02, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("B", new byte[] {0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("C", new byte[] {0x02, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("D", new byte[] {0x02, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("E", new byte[] {0x02, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("F", new byte[] {0x02, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("G", new byte[] {0x02, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("H", new byte[] {0x02, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("I", new byte[] {0x02, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("J", new byte[] {0x02, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("K", new byte[] {0x02, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("L", new byte[] {0x02, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("M", new byte[] {0x02, 0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("N", new byte[] {0x02, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("O", new byte[] {0x02, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("P", new byte[] {0x02, 0x00, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("Q", new byte[] {0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("R", new byte[] {0x02, 0x00, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("S", new byte[] {0x02, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("T", new byte[] {0x02, 0x00, 0x17, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("U", new byte[] {0x02, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("V", new byte[] {0x02, 0x00, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("W", new byte[] {0x02, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("X", new byte[] {0x02, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("Y", new byte[] {0x02, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("Z", new byte[] {0x02, 0x00, 0x1a, 0x00, 0x00, 0x00, 0x00, 0x00} ); - - kbdVal.put("1", new byte[] {0x02, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("2", new byte[] {0x02, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("3", new byte[] {0x02, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("4", new byte[] {0x02, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("5", new byte[] {0x02, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("6", new byte[] {0x02, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("7", new byte[] {0x02, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("8", new byte[] {0x02, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("9", new byte[] {0x02, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("0", new byte[] {0x02, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00} ); - - kbdVal.put("!", new byte[] {0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("@", new byte[] {0x40, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("#", new byte[] {0x40, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("$", new byte[] {0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("%", new byte[] {0x02, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("^", new byte[] {0x00, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("&", new byte[] {0x00, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("*", new byte[] {0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("(", new byte[] {0x00, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put(")", new byte[] {0x00, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00} ); - - kbdVal.put("return", new byte[] {0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("enter", new byte[] {0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("tab", new byte[] {0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("tabulator", new byte[] {0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("esc", new byte[] {0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("escape", new byte[] {0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("bckspc", new byte[] {0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("backspace", new byte[] {0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00} ); - - kbdVal.put("\t", new byte[] {0x00, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put(" ", new byte[] {0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x00} ); - - kbdVal.put("-", new byte[] {0x00, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("=", new byte[] {0x00, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("[", new byte[] {0x40, 0x00, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("]", new byte[] {0x40, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("\\", new byte[] {0x40, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put(";", new byte[] {0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("'", new byte[] {0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("`", new byte[] {0x40, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put(",", new byte[] {0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put(".", new byte[] {0x02, 0x00, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("/", new byte[] {0x02, 0x00, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00} ); - - kbdVal.put("_", new byte[] {0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("+", new byte[] {0x02, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("{", new byte[] {0x40, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("}", new byte[] {0x40, 0x00, 0x2e, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("|", new byte[] {0x40, 0x00, 0x23, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put(":", new byte[] {0x00, 0x00, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("\"", new byte[] {0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("~", new byte[] {0x40, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("<", new byte[] {0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put(">", new byte[] {0x02, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00} ); - kbdVal.put("?", new byte[] {0x02, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00} ); - - kbdVal.put("é", new byte[] {0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("è", new byte[] {0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("ç", new byte[] {0x00, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("à", new byte[] {0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("ù", new byte[] {0x00, 0x00, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("²", new byte[] {0x00, 0x00, 0x35, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("°", new byte[] {0x02, 0x00, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("£", new byte[] {0x02, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("µ", new byte[] {0x02, 0x00, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("§", new byte[] {0x02, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - - kbdVal.put("^", new byte[] {0x40, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("€", new byte[] {0x40, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - kbdVal.put("¤", new byte[] {0x40, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00} ); // French specific - } - -} \ No newline at end of file diff --git a/authorizer/src/main/java/net/tjado/authorizer/Utilities.java b/authorizer/src/main/java/net/tjado/authorizer/Utilities.java index a7d1b4285..2a1d200c1 100644 --- a/authorizer/src/main/java/net/tjado/authorizer/Utilities.java +++ b/authorizer/src/main/java/net/tjado/authorizer/Utilities.java @@ -10,7 +10,7 @@ package net.tjado.authorizer; import android.util.Log; -import net.tjado.passwdsafe.BuildConfig; +import net.tjado.passwdsafe.lib.BuildConfig; public class Utilities { diff --git a/authorizer/src/main/java/net/tjado/bluetooth/BluetoothDeviceWrapper.java b/authorizer/src/main/java/net/tjado/bluetooth/BluetoothDeviceWrapper.java index a378bc679..d2beded3a 100644 --- a/authorizer/src/main/java/net/tjado/bluetooth/BluetoothDeviceWrapper.java +++ b/authorizer/src/main/java/net/tjado/bluetooth/BluetoothDeviceWrapper.java @@ -4,10 +4,9 @@ import android.bluetooth.BluetoothDevice; import android.util.Log; +import androidx.annotation.NonNull; import androidx.core.util.Preconditions; -import net.tjado.passwdsafe.lib.Utils; - import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -68,6 +67,16 @@ public String getAddress() { return devAddress; } + private String bytesToHexString(@NonNull byte[] bytes) { + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(0xFF & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + return hexString.toString(); + } + @Override public boolean equals(Object obj) { @@ -88,7 +97,7 @@ public String getHash() { try { MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); messageDigest.update(devString.getBytes()); - devHash = Utils.bytesToHexString(messageDigest.digest()); + devHash = bytesToHexString(messageDigest.digest()); } catch (NoSuchAlgorithmException e) { Log.d(TAG, "Failed to use SHA-256 for hashing - using literal representation"); devHash = devString; diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/AbstractPasswdSafeOpenNewFileFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/AbstractPasswdSafeOpenNewFileFragment.java index 16d8f20df..7091ffc52 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/AbstractPasswdSafeOpenNewFileFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/AbstractPasswdSafeOpenNewFileFragment.java @@ -47,8 +47,6 @@ public void onPause() { super.onPause(); cancelFragment(false); - itsProgressVisible.reset(); - itsFieldsDisabled.reset(); } /** diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/BluetoothForegroundService.java b/authorizer/src/main/java/net/tjado/passwdsafe/BluetoothForegroundService.java index cb23e62b8..abba20d14 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/BluetoothForegroundService.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/BluetoothForegroundService.java @@ -31,7 +31,6 @@ import net.tjado.passwdsafe.lib.ApiCompat; import net.tjado.passwdsafe.lib.PasswdSafeUtil; import net.tjado.bluetooth.BluetoothDeviceWrapper; -import net.tjado.passwdsafe.lib.Utils; import static android.app.Notification.DEFAULT_SOUND; import static android.app.Notification.DEFAULT_VIBRATE; @@ -330,6 +329,7 @@ private void checkBluetoothState(Integer state) { } } + private final class BtServiceProfileListener implements HidDeviceController.ProfileListener { @Override public void onAppStatusChanged(boolean registered) { @@ -412,21 +412,17 @@ public void onInterruptData(BluetoothDevice device, int reportId, byte[] data, B } PasswdSafe activity = ((PasswdSafeApp) getApplication()).getActiveActivity(); - if (PasswdSafe.mTransactionManager != null && activity != null && activity.isFileOpen() && !activity.isEditMode()) { + if (PasswdSafe.mTransactionManager != null && activity != null && activity.isFileOpen()) { openFileStarted = false; PasswdSafe.mTransactionManager.handleReport(data, (rawReports) -> { for (byte[] report : rawReports) { - PasswdSafeUtil.dbginfo(TAG, "Send report: " + Utils.bytesToHexString(report)); inputHost.sendReport(device, reportId, report); } }); - } else { - if (activity != null && activity.isEditMode()) { - PasswdSafeUtil.dbginfo(TAG, "App is open - notify user inside app"); - PasswdSafeUtil.showErrorMsg(getString(R.string.fido_file_closed), new ActContext(activity)); - } else if(activity != null && !openFileStarted) { + } else { + if(activity != null && !openFileStarted) { PasswdSafeUtil.dbginfo(TAG, "App is open - notify user inside app"); // setting flag that file opening getting triggered on multiple interrupts of @@ -436,7 +432,7 @@ public void onInterruptData(BluetoothDevice device, int reportId, byte[] data, B openFileResetHandler.postDelayed(() -> openFileStarted = false, OPEN_FILE_TIMEOUT_MS); if (!activity.openDefaultFile()) { - PasswdSafeUtil.showErrorMsg(getString(R.string.fido_file_closed), new ActContext(activity)); + PasswdSafeUtil.showErrorMsg("Incoming FIDO request - please open respective PasswdSafe file!", new ActContext(activity)); } } diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/BluetoothFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/BluetoothFragment.java index d5d0920e1..d6af7ecfd 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/BluetoothFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/BluetoothFragment.java @@ -145,6 +145,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, prefs = Preferences.getSharedPrefs(getContext()); + BluetoothForegroundService btService = ((PasswdSafe) requireActivity()).btService; + cbBluetoothFeature.setChecked(Preferences.getBluetoothEnabled(prefs)); if(Preferences.getBluetoothEnabled(prefs)) { cbBluetoothFido.setEnabled(true); @@ -169,7 +171,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, cbBluetoothFido.setChecked(false); cbBluetoothFido.setEnabled(false); - BluetoothForegroundService btService = ((PasswdSafe) requireActivity()).btService; if(btService != null) { btService.stopForegroundService(); } @@ -185,7 +186,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, rvDiscoveredDevicesAdapter.notifyDataSetChanged(); } - BluetoothForegroundService btService = ((PasswdSafe) requireActivity()).btService; if (btService != null) { if(cbBluetoothFido.isChecked()) { btService.requireFidoMode(); @@ -461,9 +461,11 @@ protected void clearAvailableDevices() { public class RvPairedDevicesAdapter extends RecyclerView.Adapter { private final List devices; + private final BluetoothForegroundService btService; public RvPairedDevicesAdapter(List devices) { this.devices = devices; + this.btService = ((PasswdSafe) requireActivity()).btService; } @NonNull @@ -499,7 +501,6 @@ public void onBindViewHolder(@NonNull ViewHolderPairedDevice holder, int positio holder.btnReconnect.setText(R.string.bt_reconnect); holder.btnReconnect.setTextAppearance(requireContext(), R.style.Widget_AppCompat_Button_Colored); - BluetoothForegroundService btService = ((PasswdSafe) requireActivity()).btService; if(btService != null && btService.getConnectedDevice() != null) { BluetoothDeviceWrapper connectedDevice = new BluetoothDeviceWrapper(btService.getConnectedDevice()); if(device.equals(connectedDevice)) { @@ -511,10 +512,9 @@ public void onBindViewHolder(@NonNull ViewHolderPairedDevice holder, int positio // Reconnect in case of several paired FIDO devices holder.btnReconnect.setOnClickListener(item -> { - BluetoothForegroundService btServiceInner = ((PasswdSafe) requireActivity()).btService; - if(btServiceInner != null) { - if(btServiceInner.getConnectedDevice() != null) { - BluetoothDeviceWrapper connectedDevice = new BluetoothDeviceWrapper(btServiceInner.getConnectedDevice()); + if(btService != null) { + if(btService.getConnectedDevice() != null) { + BluetoothDeviceWrapper connectedDevice = new BluetoothDeviceWrapper(btService.getConnectedDevice()); if(device.equals(connectedDevice)) { holder.btnReconnect.setEnabled(false); holder.btnReconnect.setText(R.string.bt_connected); @@ -531,10 +531,10 @@ public void onBindViewHolder(@NonNull ViewHolderPairedDevice holder, int positio // As the standard connect does not work to switch between FIDO devices, // the Bluetooth HID profile gets reinitialized. As device is already paired // the pairing dialog is not shown and the device gets connected. - btServiceInner.pairAsFido(device.getDevice()); + btService.pairAsFido(device.getDevice()); checkBtProfileStateHandler.postDelayed(() -> { - if(btServiceInner.isAppRegistered()) { + if(btService.isAppRegistered()) { PasswdSafeUtil.dbginfo(TAG, "btService.isAppRegistered is TRUE"); } else { PasswdSafeUtil.dbginfo(TAG, "btService.isAppRegistered is FALSE"); diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/LauncherFileShortcuts.java b/authorizer/src/main/java/net/tjado/passwdsafe/LauncherFileShortcuts.java index 77fcd4409..c7bfd6e1d 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/LauncherFileShortcuts.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/LauncherFileShortcuts.java @@ -7,6 +7,7 @@ */ package net.tjado.passwdsafe; +import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; @@ -18,6 +19,7 @@ import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; +import net.tjado.passwdsafe.lib.ApiCompat; import net.tjado.passwdsafe.lib.PasswdSafeUtil; import java.util.UUID; diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafe.java b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafe.java index 137def242..fa6ce7dda 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafe.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafe.java @@ -242,7 +242,6 @@ private enum ConfirmAction private enum EditFinish { ADD_RECORD, - ADD_FIDO_RECORD, CHANGE_PASSWORD, DELETE_RECORD, EDIT_NOSAVE_RECORD, @@ -487,54 +486,62 @@ protected void onNewIntent(Intent intent) PasswdSafeUtil.dbginfo(TAG, "onNewIntent: %s", intent); switch (String.valueOf(intent.getAction())) { - case PasswdSafeUtil.VIEW_INTENT: - case Intent.ACTION_VIEW: { - final Uri openUri = PasswdSafeApp.getOpenUriFromIntent(intent); - Boolean reopen = itsFileDataFrag.useFileData( - fileData -> !fileData.getUri().getUri().equals(openUri)); - if ((reopen == null) || reopen) { - // Close and reopen the new file - itsFileDataFrag.setFileData(null); - doUpdateView(ViewMode.INIT, new PasswdLocation()); - changeInitialView(); - changeFileOpenView(intent); - } - break; + case PasswdSafeUtil.VIEW_INTENT: + case Intent.ACTION_VIEW: { + final Uri openUri = PasswdSafeApp.getOpenUriFromIntent(intent); + Boolean reopen = itsFileDataFrag.useFileData( + fileData -> !fileData.getUri().getUri().equals(openUri)); + if ((reopen == null) || reopen) { + // Close and reopen the new file + itsFileDataFrag.setFileData(null); + doUpdateView(ViewMode.INIT, new PasswdLocation()); + changeInitialView(); + changeFileOpenView(intent); } - case Intent.ACTION_SEARCH: { - setRecordQueryFilter(intent.getStringExtra(SearchManager.QUERY)); + break; + } + case Intent.ACTION_SEARCH: { + setRecordQueryFilter(intent.getStringExtra(SearchManager.QUERY)); + break; + } + case PasswdSafeUtil.SEARCH_VIEW_INTENT: { + collapseSearch(); + String data = intent.getStringExtra(SearchManager.EXTRA_DATA_KEY); + if (data == null) { break; } - case PasswdSafeUtil.SEARCH_VIEW_INTENT: { - collapseSearch(); - String data = intent.getStringExtra(SearchManager.EXTRA_DATA_KEY); - if (data == null) { - break; - } - PasswdLocation loc = null; - if (data.startsWith(PasswdRecordFilter.SEARCH_VIEW_RECORD)) { - int pfxlen = PasswdRecordFilter.SEARCH_VIEW_RECORD.length(); - final String uuid = data.substring(pfxlen); - loc = useFileData(fileData -> { - PwsRecord rec = fileData.getRecord(uuid); - if (rec == null) { - return null; - } - return new PasswdLocation(rec, fileData); - }); - } else if (data.startsWith(PasswdRecordFilter.SEARCH_VIEW_GROUP)) { - int pfxlen = PasswdRecordFilter.SEARCH_VIEW_GROUP.length(); - loc = new PasswdLocation(data.substring(pfxlen)); - } - if (loc != null) { - changeLocation(loc); - } - break; + PasswdLocation loc = null; + if (data.startsWith(PasswdRecordFilter.SEARCH_VIEW_RECORD)) { + int pfxlen = PasswdRecordFilter.SEARCH_VIEW_RECORD.length(); + final String uuid = data.substring(pfxlen); + loc = useFileData(fileData -> { + PwsRecord rec = fileData.getRecord(uuid); + if (rec == null) { + return null; + } + return new PasswdLocation(rec, fileData); + }); + } else if (data.startsWith(PasswdRecordFilter.SEARCH_VIEW_GROUP)) { + int pfxlen = PasswdRecordFilter.SEARCH_VIEW_GROUP.length(); + loc = new PasswdLocation(data.substring(pfxlen)); } - case PasswdSafeUtil.NEW_INTENT: { - changeFileNewView(intent); - break; + if (loc != null) { + changeLocation(loc); + } + break; + } + case PasswdSafeUtil.NEW_INTENT: { + changeFileNewView(intent); + break; + } + default: { + FragmentManager fragMgr = getSupportFragmentManager(); + Fragment frag = fragMgr.findFragmentById(R.id.content); + if (frag instanceof PasswdSafeOpenFileFragment) { + ((PasswdSafeOpenFileFragment)frag).onNewIntent(intent); } + break; + } } } @@ -1574,16 +1581,6 @@ public void finishEditRecord(EditRecordResult result) null, result.itsNewLocation, null); } - public void finishEditFidoRecord(EditRecordResult result) - { - finishEdit(result.itsIsNewRecord ? - EditFinish.ADD_FIDO_RECORD : - (result.itsIsSave ? - EditFinish.EDIT_SAVE_RECORD : - EditFinish.EDIT_NOSAVE_RECORD), - null, result.itsNewLocation, null); - } - public void finishEditRecord(boolean save, PasswdLocation newLocation, boolean popBack) { EditFinish finish = EditFinish.EDIT_NOSAVE_RECORD; @@ -1930,7 +1927,7 @@ private void editFinished(FinishSaveInfo saveState) if (contentsFrag instanceof PasswdSafeListFragment) { ((PasswdSafeListFragment)contentsFrag).updateSelection(saveState.itsNewLocation); } else if (contentsFrag instanceof PasswdSafeListFragmentTree) { - changeOpenView(itsLocation, OpenViewChange.REFRESH); + /* TODO: implement me */ } } } else if (saveState.shouldResetLoc(itsFileDataFrag.getFileDataView(), @@ -1993,7 +1990,8 @@ private void changeOpenView(PasswdLocation location, OpenViewChange change) viewFrag = PasswdSafeListFragment.newInstance(location, true); } - if (change == OpenViewChange.VIEW) { + if (change == OpenViewChange.INITIAL) { + } else if (change == OpenViewChange.VIEW) { viewMode = ChangeMode.OPEN; } else if (change == OpenViewChange.REFRESH) { viewMode = ChangeMode.REFRESH_LIST; @@ -2151,7 +2149,7 @@ private void doChangeView(final ChangeMode mode, // is the current fragment is the target fragment, skip change if no refresh if(!refresh && currFrag != null && contentFrag != null && currFrag.getClass() == contentFrag.getClass() && - mode != ChangeMode.VIEW_PREFERENCES && itsIsDisplayListTreeView + mode != ChangeMode.VIEW_PREFERENCES ){ return; } @@ -2568,10 +2566,6 @@ private void checkBluetoothState(Integer state) { } } - public boolean isEditMode() { - return (itsCurrViewMode == ViewMode.EDIT_RECORD); - } - /** * Information for finishing the save of the file */ @@ -2599,12 +2593,6 @@ private FinishSaveInfo(EditFinish task, itsIsPopBack = true; break; } - case ADD_FIDO_RECORD: { - itsIsAddRecord = true; - itsIsSave = true; - itsIsPopBack = false; - break; - } case CHANGE_PASSWORD: case DELETE_RECORD: case EDIT_SAVE_RECORD: { diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeEditRecordFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeEditRecordFragment.java index 581d81fea..84d7c2817 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeEditRecordFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeEditRecordFragment.java @@ -17,7 +17,6 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.textfield.TextInputLayout; @@ -462,22 +461,7 @@ public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.menu_save) { - - if(itsValidator.isPasswordValid()) { - saveRecord(); - return true; - } - - AlertDialog.Builder alert = new AlertDialog.Builder(requireContext()) - .setTitle(getString(R.string.confirm)) - .setMessage("Save with empty password?") - .setPositiveButton(R.string.confirm, - (dialog, whichButton) -> { - saveRecord(); - }) - .setNegativeButton(R.string.cancel, null); - alert.show(); - + saveRecord(); return true; } else if (itemId == R.id.menu_protect) { itsIsProtected = !itsIsProtected; @@ -1650,7 +1634,6 @@ private boolean isKeyboardVisible(View rootView) { private class Validator extends AbstractTextWatcher { private boolean itsIsValid = false; - private boolean itsIsPasswordValid = false; private boolean itsIsPaused = false; /** @@ -1712,7 +1695,8 @@ protected final void validate() itsTypeError.setText(typeError); boolean valid = (typeError == null); - valid &= !TextInputUtils.setTextInputError(validateTitle(), itsTitleInput); + valid &= !TextInputUtils.setTextInputError(validateTitle(), + itsTitleInput); String group = getGroupVal(); String groupError = null; @@ -1723,8 +1707,10 @@ protected final void validate() itsGroupError.setText(groupError); valid &= (groupError == null); - itsIsPasswordValid = !TextInputUtils.setTextInputError(validatePassword(), itsPasswordInput); - valid &= !TextInputUtils.setTextInputError(validatePasswordConfirm(), itsPasswordConfirmInput); + valid &= !TextInputUtils.setTextInputError(validatePassword(), + itsPasswordInput); + valid &= !TextInputUtils.setTextInputError( + validatePasswordConfirm(), itsPasswordConfirmInput); if (itsIsV3) { boolean warnExpiryDate = false; @@ -1776,14 +1762,6 @@ protected final boolean isValid() return itsIsValid; } - /** - * Is password valid - */ - protected final boolean isPasswordValid() - { - return itsIsPasswordValid; - } - @Override public final void afterTextChanged(Editable s) { @@ -1816,10 +1794,18 @@ private String validateTitle() */ private String validatePassword() { - if (itsRecType == PasswdRecord.Type.NORMAL && itsPassword.getText().length() == 0) { - return getString(R.string.empty_password); + switch (itsRecType) { + case NORMAL: { + if (itsPassword.getText().length() == 0) { + return getString(R.string.empty_password); + } + break; + } + case ALIAS: + case SHORTCUT: { + break; + } } - return null; } diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeListFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeListFragment.java index 00c650446..6c126a031 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeListFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeListFragment.java @@ -30,6 +30,7 @@ import android.widget.SectionIndexer; import android.widget.TextView; +import net.tjado.passwdsafe.lib.PasswdSafeUtil; import net.tjado.passwdsafe.lib.view.GuiUtils; import net.tjado.passwdsafe.view.CopyField; import net.tjado.passwdsafe.view.PasswdLocation; diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeNewFileFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeNewFileFragment.java index 57dddb238..af281d880 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeNewFileFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeNewFileFragment.java @@ -324,6 +324,36 @@ protected final void doResolveTaskFinished() titleId = R.string.new_local_file; break; } + case SYNC_PROVIDER: { + if (uri.getSyncType() == null) { + PasswdSafeUtil.showFatalMsg("Unknown sync type", + getActivity()); + break; + } + switch (uri.getSyncType()) { + case GDRIVE: { + titleId = R.string.new_drive_file; + break; + } + case DROPBOX: { + titleId = R.string.new_dropbox_file; + break; + } + case BOX: { + titleId = R.string.new_box_file; + break; + } + case ONEDRIVE: { + titleId = R.string.new_onedrive_file; + break; + } + case OWNCLOUD: { + titleId = R.string.new_owncloud_file; + break; + } + } + break; + } case EMAIL: case GENERIC_PROVIDER: case BACKUP: { diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileFragment.java index 8718d8a1c..4983c75dc 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileFragment.java @@ -16,7 +16,6 @@ import android.os.Bundle; import android.os.Build; import android.os.Looper; -import android.provider.Settings; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; @@ -38,14 +37,15 @@ import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.StringRes; import androidx.annotation.CheckResult; import androidx.appcompat.widget.AppCompatImageView; import androidx.biometric.BiometricPrompt; import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import com.google.android.material.textfield.TextInputLayout; +import net.tjado.passwdsafe.PasswdSafeOpenFileViewModel.SavedPasswordState; import net.tjado.passwdsafe.file.PasswdFileData; import net.tjado.passwdsafe.file.PasswdFileUri; import net.tjado.passwdsafe.lib.ActContext; @@ -55,9 +55,7 @@ import net.tjado.passwdsafe.lib.view.TextInputUtils; import net.tjado.passwdsafe.lib.view.TypefaceUtils; import net.tjado.passwdsafe.util.Pair; -import net.tjado.passwdsafe.util.SavedPasswordState; import net.tjado.passwdsafe.view.ConfirmPromptDialog; -import com.yubico.yubikit.core.YubiKeyDevice; import org.pwsafe.lib.exception.InvalidPassphraseException; import org.pwsafe.lib.file.Owner; @@ -68,6 +66,7 @@ import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.util.ArrayList; +import java.util.Objects; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -79,7 +78,8 @@ */ public class PasswdSafeOpenFileFragment extends AbstractPasswdSafeOpenNewFileFragment - implements ConfirmPromptDialog.Listener, + implements Observer, + ConfirmPromptDialog.Listener, View.OnClickListener, CompoundButton.OnCheckedChangeListener { /** @@ -112,29 +112,7 @@ private enum Phase YUBIKEY, OPENING, SAVING_PASSWORD, - FINISHED; - - /** - * Is the phase where the YubiKey is active - */ - public boolean isYubikeyActive() - { - switch (this) { - case YUBIKEY: { - return true; - } - case INITIAL: - case CHECKING_YUBIKEY: - case RESOLVING: - case WAITING_PASSWORD: - case OPENING: - case SAVING_PASSWORD: - case FINISHED: { - break; - } - } - return false; - } + FINISHED } /** @@ -160,8 +138,6 @@ private enum SavePasswordChange private CheckBox itsReadonlyCb; private CheckBox itsSavePasswdCb; private CheckBox itsYubikeyCb; - private TextView itsYubikeyError; - private TextView itsYubikeyProgressMsg; private Button itsOpenBtn; private SavedPasswordsMgr itsSavedPasswordsMgr; private SavePasswordChange itsSaveChange = SavePasswordChange.NONE; @@ -173,7 +149,6 @@ private enum SavePasswordChange private TextWatcher itsErrorClearingWatcher; private PasswdSafeOpenFileViewModel itsOpenModel; - private YubikeyViewModel itsYubikeyModel; private static final String ARG_URI = "uri"; private static final String ARG_REC_TO_OPEN = "recToOpen"; @@ -206,11 +181,9 @@ public void onCreate(Bundle savedInstanceState) } setDoResolveOnStart(false); - var viewModelProvider = new ViewModelProvider(requireActivity()); - itsOpenModel = viewModelProvider.get(PasswdSafeOpenFileViewModel.class); - itsOpenModel.getData().observe(this, this::onOpenViewModelDataChanged); - itsYubikeyModel = viewModelProvider.get(YubikeyViewModel.class); - itsYubikeyModel.getDeviceData().observe(this, this::onYubikeyDeviceChanged); + itsOpenModel = new ViewModelProvider(requireActivity()).get( + PasswdSafeOpenFileViewModel.class); + itsOpenModel.getData().observe(this, this); } /* http://stackoverflow.com/a/27672844 @@ -302,16 +275,13 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, GuiUtils.setVisible(itsSavePasswdCb, saveAvailable); GuiUtils.setVisible(itsSavedPasswordMsg, false); - itsYubiMgr = new YubikeyMgr(itsYubikeyModel, this); + itsYubiMgr = new YubikeyMgr(); itsYubikeyCb = rootView.findViewById(R.id.yubikey); itsYubikeyCb.setOnCheckedChangeListener(this); - itsYubikeyError = rootView.findViewById(R.id.yubikey_error); - GuiUtils.setVisible(itsYubikeyError, false); - itsYubikeyProgressMsg = rootView.findViewById(R.id.yubi_progress_text); - GuiUtils.setVisible(itsYubikeyProgressMsg, false); - setVisibility(R.id.yubikey_nfc_disabled, false, rootView); setVisibility(R.id.file_open_help_text, false, rootView); + setVisibility(R.id.yubi_progress_text, false, rootView); + return rootView; } @@ -347,33 +317,8 @@ public void onStart() @Override public void onResume() { - PasswdSafeUtil.dbginfo(TAG, "onResume"); - super.onResume(); itsListener.updateViewFileOpen(); - - switch (itsPhase) { - case RESOLVING: { - var openData = itsOpenModel.getDataValue(); - if (!openData.isResolved()) { - startResolve(); - } - break; - } - case YUBIKEY: { - setProgressVisible(true, false); - setFieldsDisabled(true); - break; - } - case INITIAL: - case CHECKING_YUBIKEY: - case WAITING_PASSWORD: - case OPENING: - case SAVING_PASSWORD: - case FINISHED: { - break; - } - } } @Override @@ -407,6 +352,22 @@ public void onDetach() } } + /** Handle a new intent */ + public void onNewIntent(Intent intent) + { + if (itsYubiMgr != null) { + itsYubiMgr.handleKeyIntent(intent); + } + } + + @Override + public void onChanged(@Nullable final PasswdSafeOpenFileViewModel.OpenData openData) + { + if (openData != null) { + PasswdSafeUtil.dbginfo(TAG, "onChanged phase: %s, data: %s", itsPhase, openData); + } + } + @Override public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) @@ -415,32 +376,18 @@ public void onCreateOptionsMenu(@NonNull Menu menu, inflater.inflate(R.menu.fragment_passwdsafe_open_file, menu); var data = itsOpenModel.getDataValue(); - boolean yubiEnabled = false; - boolean yubiNfcAvailable = false; switch (data.getYubiState()) { - case USB_DISABLED_NFC_ENABLED: - case USB_ENABLED_NFC_DISABLED: - case ENABLED: { - yubiEnabled = true; - yubiNfcAvailable = true; - break; - } - case USB_ENABLED_NFC_UNAVAILABLE: { - yubiEnabled = true; - break; - } - case USB_DISABLED_NFC_DISABLED: { - yubiNfcAvailable = true; + case ENABLED: + case DISABLED: { break; } case UNKNOWN: case UNAVAILABLE: { + menu.setGroupVisible(R.id.menu_group_slots, false); break; } } - menu.setGroupVisible(R.id.menu_group_slots, yubiEnabled); - MenuItem item; switch (data.getYubiSlot()) { case 2: @@ -454,9 +401,6 @@ public void onCreateOptionsMenu(@NonNull Menu menu, } } item.setChecked(true); - - item = menu.findItem(R.id.menu_nfc_settings); - item.setVisible(yubiNfcAvailable); } super.onCreateOptionsMenu(menu, inflater); @@ -484,14 +428,6 @@ public boolean onOptionsItemSelected(MenuItem item) item.setChecked(true); itsOpenModel.setYubiSlot(2); return true; - } else if (itemId == R.id.menu_nfc_settings) { - try { - var intent = new Intent(Settings.ACTION_NFC_SETTINGS); - requireActivity().startActivity(intent); - } catch (Exception e) { - PasswdSafeUtil.dbginfo(TAG, e, "NFC activity not started"); - } - return true; } return super.onOptionsItemSelected(item); } @@ -600,7 +536,19 @@ protected final void doSetFieldsEnabled(boolean enabled) itsPasswordEdit.setEnabled(enabled); itsOpenBtn.setEnabled(enabled); itsSavePasswdCb.setEnabled(enabled && openData.isSaveAllowed()); - itsYubikeyCb.setEnabled(openData.getYubiState().isEnabled() && enabled); + + switch (openData.getYubiState()) { + case ENABLED: { + itsYubikeyCb.setEnabled(enabled); + break; + } + case UNKNOWN: + case UNAVAILABLE: + case DISABLED: { + itsYubikeyCb.setEnabled(false); + break; + } + } } /** @@ -619,7 +567,6 @@ private void setPhase(Phase newPhase) break; } case WAITING_PASSWORD: { - itsOpenModel.setYubikeyError(null); try (Owner password = PwsPassword.create(itsPasswordEdit)) { setOpenPassword(password.pass(), false); @@ -627,7 +574,12 @@ private void setPhase(Phase newPhase) break; } case YUBIKEY: { - exitYubikeyPhase(); + View root = getView(); + setVisibility(R.id.yubi_progress_text, false, + Objects.requireNonNull(root)); + setProgressVisible(false, false); + setFieldsDisabled(false); + itsYubiMgr.stop(); break; } case SAVING_PASSWORD: { @@ -658,7 +610,12 @@ private void setPhase(Phase newPhase) break; } case YUBIKEY: { - enterYubikeyPhase(); + itsYubiUser = new YubikeyUser(); + itsYubiMgr.start(itsYubiUser); + View root = requireView(); + setVisibility(R.id.yubi_progress_text, true, root); + setProgressVisible(true, false); + setFieldsDisabled(true); break; } case OPENING: { @@ -703,73 +660,36 @@ private void exitResolvingPhase() private void enterCheckingYubikeyPhase() { var openData = itsOpenModel.getDataValue(); - var state = itsYubikeyModel.getState(requireContext()); if (!openData.hasYubiInfo()) { + var state = itsYubiMgr.getState(getActivity()); var prefs = Preferences.getSharedPrefs(getContext()); itsOpenModel.provideYubiInfo(state, Preferences.getFileOpenYubikeyPref( prefs)); openData = itsOpenModel.getDataValue(); - } else if (state != openData.getYubiState()) { - itsOpenModel.provideYubiInfo(state, openData.isYubikeySelected()); - openData = itsOpenModel.getDataValue(); } - boolean yubikeyVisible = false; - boolean yubikeyEnabled = false; - String yubikeySfx = null; - boolean yubikeyNfcDisabledVisible = false; - switch (openData.getYubiState()) { - case UNKNOWN: - case UNAVAILABLE: { - break; - } - case USB_DISABLED_NFC_ENABLED: { - yubikeyVisible = true; - yubikeyEnabled = true; - yubikeySfx = "NFC"; - break; - } - case USB_DISABLED_NFC_DISABLED: { - yubikeyVisible = true; - yubikeySfx = "NFC"; - yubikeyNfcDisabledVisible = true; - break; - } - case USB_ENABLED_NFC_UNAVAILABLE: { - yubikeyVisible = true; - yubikeyEnabled = true; - yubikeySfx = "USB"; - break; - } - case USB_ENABLED_NFC_DISABLED: { - yubikeyVisible = true; - yubikeyEnabled = true; - yubikeySfx = "USB"; - yubikeyNfcDisabledVisible = true; + case UNKNOWN: + case UNAVAILABLE: { + GuiUtils.setVisible(itsYubikeyCb, false); break; - } - case ENABLED: { - yubikeyVisible = true; - yubikeyEnabled = true; - yubikeySfx = "NFC\u00A0/\u00A0USB"; // Non-breaking spaces - break; } + case DISABLED: { + GuiUtils.setVisible(itsYubikeyCb, true); + itsYubikeyCb.setEnabled(false); + itsYubikeyCb.setText(R.string.yubikey_disabled); + itsYubikeyCb.setChecked(false); + break; + } + case ENABLED: { + GuiUtils.setVisible(itsYubikeyCb, true); + itsYubikeyCb.setEnabled(true); + itsYubikeyCb.setText(R.string.yubikey); + itsYubikeyCb.setChecked(openData.isYubikeySelected()); + break; } - - GuiUtils.setVisible(itsYubikeyCb, yubikeyVisible); - itsYubikeyCb.setEnabled(yubikeyEnabled); - itsYubikeyCb.setChecked(yubikeyEnabled && openData.isYubikeySelected()); - StringBuilder yubikeyText = - new StringBuilder(getString(R.string.yubikey)); - if (yubikeySfx != null) { - yubikeyText.append(" (").append(yubikeySfx).append(")"); } - itsYubikeyCb.setText(yubikeyText); - var rootView = requireView(); - setVisibility(R.id.yubikey_nfc_disabled, yubikeyNfcDisabledVisible, - rootView); setPhase(Phase.RESOLVING); } @@ -884,29 +804,6 @@ private void enterWaitingPasswordPhase() } } - /** - * Enter the Yubikey phase - */ - private void enterYubikeyPhase() - { - itsYubiUser = new YubikeyUser(); - itsYubiMgr.start(itsYubiUser); - updateYubikeyProgress(null, Boolean.TRUE); - setProgressVisible(true, false); - setFieldsDisabled(true); - } - - /** - * Exit the Yubikey phase - */ - private void exitYubikeyPhase() - { - updateYubikeyProgress(null, Boolean.FALSE); - setProgressVisible(false, false); - setFieldsDisabled(false); - itsYubiMgr.stop(); - } - /** * Enter the opening phase */ @@ -1105,66 +1002,6 @@ private void finishFileOpen(PasswdFileData fileData) itsListener.handleFileOpen(fileData, itsRecToOpen); } - /** - * Handle a change in the open file data model - */ - private void onOpenViewModelDataChanged( - @Nullable PasswdSafeOpenFileViewModelData openData) - { - Throwable yubikeyError = null; - if (openData != null) { - PasswdSafeUtil.dbginfo(TAG, "onChanged phase: %s, data: %s", - itsPhase, openData); - yubikeyError = openData.getYubikeyError(); - } - - itsYubikeyError.setText(getString(R.string.yubikey_error, - yubikeyError)); - GuiUtils.setVisible(itsYubikeyError, yubikeyError != null); - } - - /** - * Handle a change in the YubiKey device model - */ - private void onYubikeyDeviceChanged(YubiKeyDevice device) - { - PasswdSafeUtil.dbginfo(TAG, "YubiDevice changed: %s", YubikeyViewModel.toString(device)); - updateYubikeyProgress(YubikeyViewModel.isUsbYubikey(device), null); - } - - /** - * Update the YubiKey progress message - * - * @param hasUsbDevice Non-null if the presence of a USB YubiKey is known - * @param isYubikeyPhase Non-null if the phase is known to be Yubikey or not - */ - private void updateYubikeyProgress(@Nullable Boolean hasUsbDevice, - @Nullable Boolean isYubikeyPhase) - { - boolean hasUsbDeviceVal = (hasUsbDevice != null) ? hasUsbDevice : - itsYubikeyModel.isUsbYubikeyDevice(); - boolean isYubikeyPhaseVal = (isYubikeyPhase != null) ? isYubikeyPhase : - itsPhase.isYubikeyActive(); - - @StringRes int text = R.string.press_yubikey; - if (hasUsbDeviceVal) { - if (isYubikeyPhaseVal) { - text = R.string.usb_yubikey_present_check_button; - } else { - text = R.string.usb_yubikey_present; - } - } else { - if (isYubikeyPhaseVal && - itsOpenModel.getDataValue().getYubiState().isUsbEnabled()) { - text = R.string.press_or_insert_yubikey; - } - } - - itsYubikeyProgressMsg.setText(text); - GuiUtils.setVisible(itsYubikeyProgressMsg, - hasUsbDeviceVal | isYubikeyPhaseVal); - } - /** * Set the title */ @@ -1315,10 +1152,9 @@ protected void onTaskFinished(OpenResult result, private class YubikeyUser implements YubikeyMgr.User { @Override - @NonNull public Activity getActivity() { - return PasswdSafeOpenFileFragment.this.requireActivity(); + return PasswdSafeOpenFileFragment.this.getActivity(); } @Override @CheckResult @Nullable @@ -1332,17 +1168,15 @@ public void finish(Owner.Param password, Exception e) { boolean haveUser = (itsYubiUser != null); itsYubiUser = null; - if (haveUser) { - Exception yubikeyError = null; - var nextPhase = Phase.WAITING_PASSWORD; - if (password != null) { - setOpenPassword(password, true); - nextPhase = Phase.OPENING; - } else if (e != null) { - yubikeyError = e; - } - itsOpenModel.setYubikeyError(yubikeyError); - setPhase(nextPhase); + if (password != null) { + setOpenPassword(password, true); + setPhase(Phase.OPENING); + } else if (e != null) { + Activity act = getActivity(); + PasswdSafeUtil.showFatalMsg( + e, act.getString(R.string.yubikey_error), act); + } else if (haveUser) { + setPhase(Phase.WAITING_PASSWORD); } } diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileViewModel.java b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileViewModel.java index 1d6a02cba..afab67710 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileViewModel.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileViewModel.java @@ -7,6 +7,8 @@ */ package net.tjado.passwdsafe; +import android.annotation.SuppressLint; +import androidx.annotation.CheckResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; @@ -15,12 +17,12 @@ import net.tjado.passwdsafe.file.PasswdFileUri; import net.tjado.passwdsafe.lib.PasswdSafeUtil; -import net.tjado.passwdsafe.util.SavedPasswordState; import net.tjado.passwdsafe.util.YubiState; import org.pwsafe.lib.file.Owner; import org.pwsafe.lib.file.PwsPassword; +import java.io.Closeable; import java.util.Objects; /** @@ -30,8 +32,213 @@ public class PasswdSafeOpenFileViewModel extends ViewModel { private static final String TAG = "PasswdSafeOpenFileVM"; - - private final MutableLiveData itsData; + public enum SavedPasswordState + { + UNKNOWN, + NOT_AVAILABLE, + AVAILABLE, + LOADED_SUCCESS, + LOADED_FAILURE + } + + /** + * View model data for opening a file + */ + public static class OpenData implements Closeable + { + private boolean itsIsResolved = false; + private PasswdFileUri itsPasswdFileUri; + private boolean itsIsSaveAllowed = false; + + private boolean itsHasYubiState = false; + private YubiState itsYubiState = YubiState.UNKNOWN; + private int itsYubiSlot = 2; + private boolean itsIsYubikeySelected = false; + + private int itsRetries = 0; + private static final int NUM_RETRIES = 5; + + private SavedPasswordState itsSavedPasswordState = + SavedPasswordState.UNKNOWN; + private CharSequence itsLoadedPasswordMsg = null; + private Owner itsLoadedPassword; + + private Owner itsOpenPassword; + private boolean itsIsOpenYubikey = false; + + /** + * Constructor + */ + private OpenData() + { + } + + /** + * Copy constructor + */ + private OpenData(@NonNull OpenData data) + { + itsIsResolved = data.itsIsResolved; + itsPasswdFileUri = data.itsPasswdFileUri; + itsIsSaveAllowed = data.itsIsSaveAllowed; + + itsHasYubiState = data.itsHasYubiState; + itsYubiState = data.itsYubiState; + itsYubiSlot = data.itsYubiSlot; + itsIsYubikeySelected = data.itsIsYubikeySelected; + + itsRetries = data.itsRetries; + + itsSavedPasswordState = data.itsSavedPasswordState; + itsLoadedPasswordMsg = data.itsLoadedPasswordMsg; + setLoadedPassword((data.itsLoadedPassword != null) ? + data.itsLoadedPassword.pass() : null); + + setOpenPassword((data.itsOpenPassword != null) ? + data.itsOpenPassword.pass() : null); + itsIsOpenYubikey = data.itsIsOpenYubikey; + } + + /** + * Finalize the data + */ + protected void finalize() + { + PasswdSafeUtil.dbginfo(TAG, "data finalize"); + close(); + } + + boolean isResolved() + { + return itsIsResolved; + } + + @Nullable + PasswdFileUri getUri() + { + return itsPasswdFileUri; + } + + boolean isSaveAllowed() + { + return itsIsSaveAllowed; + } + + boolean hasYubiInfo() + { + return itsHasYubiState; + } + + YubiState getYubiState() + { + return itsYubiState; + } + + int getYubiSlot() + { + return itsYubiSlot; + } + + boolean isYubikeySelected() + { + return itsIsYubikeySelected; + } + + boolean hasPasswordRetry() + { + return itsRetries > 0; + } + + SavedPasswordState getSavedPasswordState() + { + return itsSavedPasswordState; + } + + @Nullable + CharSequence getLoadedPasswordMsg() + { + return itsLoadedPasswordMsg; + } + + @CheckResult @Nullable + public Owner getLoadedPassword() + { + return (itsLoadedPassword != null) ? + itsLoadedPassword.pass().use() : null; + } + + @CheckResult @Nullable + public Owner getOpenPassword() + { + return (itsOpenPassword != null) ? itsOpenPassword.pass().use() : + null; + } + + public boolean isOpenYubikey() + { + return itsIsOpenYubikey; + } + + @NonNull + @SuppressLint("DefaultLocale") + public String toString() + { + return String.format( + "{\nuri: %s, save allowed: %b, retries: %d" + + "\nyubi state: %s, slot: %d, selected: %b" + + "\nsaved passwd: %s, loaded passwd: %b, loaded msg: %s"+ + "\nopen passwd %b, open yubikey %b}", + itsPasswdFileUri, itsIsSaveAllowed, itsRetries, + itsYubiState, itsYubiSlot, itsIsYubikeySelected, + itsSavedPasswordState, (itsLoadedPassword != null), + itsLoadedPasswordMsg, + (itsOpenPassword != null), itsIsOpenYubikey); + } + + /** + * Close the view model data and release resources + */ + @Override + public void close() + { + setOpenPassword(null); + setLoadedPassword(null); + } + + /** + * Set the open password + */ + private void setOpenPassword( + @Nullable Owner.Param password) + { + if (itsOpenPassword != null) { + itsOpenPassword.close(); + itsOpenPassword = null; + } + + if (password != null) { + itsOpenPassword = password.use(); + } + } + + /** + * Set the loaded password + */ + private void setLoadedPassword( + @Nullable Owner.Param password) + { + if (itsLoadedPassword != null) { + itsLoadedPassword.close(); + itsLoadedPassword = null; + } + + if (password != null) { + itsLoadedPassword = password.use(); + } + } + } + + private final MutableLiveData itsData; /** * Constructor @@ -39,7 +246,7 @@ public class PasswdSafeOpenFileViewModel extends ViewModel public PasswdSafeOpenFileViewModel() { PasswdSafeUtil.dbginfo(TAG, "ctor"); - itsData = new MutableLiveData<>(new PasswdSafeOpenFileViewModelData()); + itsData = new MutableLiveData<>(new OpenData()); } /** @@ -47,7 +254,11 @@ public PasswdSafeOpenFileViewModel() */ public void provideYubiInfo(YubiState state, boolean selected) { - setDataValue(getDataValue().cloneWithYubiInfo(state, selected)); + OpenData newData = new OpenData(getDataValue()); + newData.itsHasYubiState = true; + newData.itsYubiState = state; + newData.itsIsYubikeySelected = selected; + setDataValue(newData); } /** @@ -55,7 +266,11 @@ public void provideYubiInfo(YubiState state, boolean selected) */ public void provideResolveResults(PasswdFileUri uri, boolean saveAllowed) { - setDataValue(getDataValue().cloneWithResolveResults(uri, saveAllowed)); + OpenData newData = new OpenData(getDataValue()); + newData.itsIsResolved = true; + newData.itsPasswdFileUri = uri; + newData.itsIsSaveAllowed = saveAllowed; + setDataValue(newData); } /** @@ -63,7 +278,9 @@ public void provideResolveResults(PasswdFileUri uri, boolean saveAllowed) */ public void setYubiSelected(boolean selected) { - setDataValue(getDataValue().cloneWithYubikeySelection(selected)); + OpenData newData = new OpenData(getDataValue()); + newData.itsIsYubikeySelected = selected; + setDataValue(newData); } /** @@ -71,18 +288,9 @@ public void setYubiSelected(boolean selected) */ public void setYubiSlot(int slot) { - setDataValue(getDataValue().cloneWithYubikeySlot(slot)); - } - - /** - * Set an error using the YubiKey - */ - public void setYubikeyError(Throwable error) - { - var currData = getDataValue(); - if (!Objects.equals(error, currData.getYubikeyError())) { - setDataValue(currData.cloneWithYubikeyError(error)); - } + OpenData newData = new OpenData(getDataValue()); + newData.itsYubiSlot = slot; + setDataValue(newData); } /** @@ -90,10 +298,11 @@ public void setYubikeyError(Throwable error) */ public boolean checkOpenRetries() { - var currData = getDataValue(); - int retries = currData.getPasswordRetries(); - if (retries < PasswdSafeOpenFileViewModelData.NUM_RETRIES) { - setDataValue(currData.cloneWithPasswordRetries(retries + 1)); + OpenData data = getDataValue(); + if (data.itsRetries < OpenData.NUM_RETRIES) { + OpenData newData = new OpenData(data); + newData.itsRetries++; + setDataValue(newData); return true; } else { return false; @@ -108,7 +317,11 @@ public void setSavedPasswordState( @Nullable CharSequence loadedMsg, @Nullable Owner.Param loadedPassword) { - setDataValue(getDataValue().cloneWithSavedPasswordState(state, loadedMsg, loadedPassword)); + var newData = new OpenData(getDataValue()); + newData.itsSavedPasswordState = state; + newData.itsLoadedPasswordMsg = loadedMsg; + newData.setLoadedPassword(loadedPassword); + setDataValue(newData); } /** @@ -117,13 +330,16 @@ public void setSavedPasswordState( public void setOpenPassword(@Nullable Owner.Param password, boolean fromYubikey) { - setDataValue(getDataValue().cloneWithOpenPassword(password, fromYubikey)); + var newData = new OpenData(getDataValue()); + newData.setOpenPassword(password); + newData.itsIsOpenYubikey = fromYubikey; + setDataValue(newData); } /** * Get the live open data */ - public LiveData getData() + public LiveData getData() { return itsData; } @@ -131,7 +347,8 @@ public LiveData getData() /** * Get the current value of the open data */ - public @NonNull PasswdSafeOpenFileViewModelData getDataValue() + public @NonNull + OpenData getDataValue() { return Objects.requireNonNull(itsData.getValue()); } @@ -141,13 +358,13 @@ public LiveData getData() */ public void resetData() { - setDataValue(new PasswdSafeOpenFileViewModelData()); + setDataValue(new OpenData()); } /** * Set a new value for the open data */ - private void setDataValue(@NonNull PasswdSafeOpenFileViewModelData newData) + private void setDataValue(@NonNull OpenData newData) { var data = itsData.getValue(); if (data != null) { diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileViewModelData.java b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileViewModelData.java deleted file mode 100644 index b3cb0b84a..000000000 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeOpenFileViewModelData.java +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright (©) 2023 Jeff Harris - * All rights reserved. Use of the code is allowed under the - * Artistic License 2.0 terms, as specified in the LICENSE file - * distributed with this code, or available from - * http://www.opensource.org/licenses/artistic-license-2.0.php - */ -package net.tjado.passwdsafe; - -import android.annotation.SuppressLint; -import androidx.annotation.CheckResult; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import net.tjado.passwdsafe.file.PasswdFileUri; -import net.tjado.passwdsafe.lib.PasswdSafeUtil; -import net.tjado.passwdsafe.util.SavedPasswordState; -import net.tjado.passwdsafe.util.YubiState; - -import org.pwsafe.lib.file.Owner; -import org.pwsafe.lib.file.PwsPassword; - -import java.io.Closeable; - -/** - * View model data for opening a file - */ -public class PasswdSafeOpenFileViewModelData implements Closeable -{ - public static final int NUM_RETRIES = 5; - - private boolean itsIsResolved = false; - private PasswdFileUri itsPasswdFileUri; - private boolean itsIsSaveAllowed = false; - - private boolean itsHasYubiState = false; - private YubiState itsYubiState = YubiState.UNKNOWN; - private int itsYubiSlot = 2; - private boolean itsIsYubikeySelected = false; - private Throwable itsYubikeyError = null; - - private int itsRetries = 0; - - private SavedPasswordState itsSavedPasswordState = - SavedPasswordState.UNKNOWN; - private CharSequence itsLoadedPasswordMsg = null; - private Owner itsLoadedPassword; - - private Owner itsOpenPassword; - private boolean itsIsOpenYubikey = false; - - private static final String TAG = "PasswdSafeOpenFileVMData"; - - /** - * Constructor - */ - public PasswdSafeOpenFileViewModelData() - { - } - - /** - * Copy constructor - */ - private PasswdSafeOpenFileViewModelData( - @NonNull PasswdSafeOpenFileViewModelData data) - { - itsIsResolved = data.itsIsResolved; - itsPasswdFileUri = data.itsPasswdFileUri; - itsIsSaveAllowed = data.itsIsSaveAllowed; - - itsHasYubiState = data.itsHasYubiState; - itsYubiState = data.itsYubiState; - itsYubiSlot = data.itsYubiSlot; - itsIsYubikeySelected = data.itsIsYubikeySelected; - itsYubikeyError = data.itsYubikeyError; - - itsRetries = data.itsRetries; - - itsSavedPasswordState = data.itsSavedPasswordState; - itsLoadedPasswordMsg = data.itsLoadedPasswordMsg; - setLoadedPassword((data.itsLoadedPassword != null) ? - data.itsLoadedPassword.pass() : null); - - setOpenPassword( - (data.itsOpenPassword != null) ? data.itsOpenPassword.pass() : - null); - itsIsOpenYubikey = data.itsIsOpenYubikey; - } - - /** - * Clone with Yubikey state - */ - public PasswdSafeOpenFileViewModelData cloneWithYubiInfo( - YubiState yubiState, - boolean yubikeySelected) - { - var newData = new PasswdSafeOpenFileViewModelData(this); - newData.itsHasYubiState = true; - newData.itsYubiState = yubiState; - newData.itsIsYubikeySelected = yubikeySelected; - return newData; - } - - /** - * Clone with file URI resolving info - */ - public PasswdSafeOpenFileViewModelData cloneWithResolveResults( - PasswdFileUri uri, - boolean saveAllowed) - { - var newData = new PasswdSafeOpenFileViewModelData(this); - newData.itsIsResolved = true; - newData.itsPasswdFileUri = uri; - newData.itsIsSaveAllowed = saveAllowed; - return newData; - } - - /** - * Clone with Yubikey selection - */ - public PasswdSafeOpenFileViewModelData cloneWithYubikeySelection( - boolean selected) - { - var newData = new PasswdSafeOpenFileViewModelData(this); - newData.itsIsYubikeySelected = selected; - return newData; - } - - /** - * Clone with Yubikey slot - */ - public PasswdSafeOpenFileViewModelData cloneWithYubikeySlot(int slot) - { - var newData = new PasswdSafeOpenFileViewModelData(this); - newData.itsYubiSlot = slot; - return newData; - } - - /** - * Clone with Yubikey error - */ - public PasswdSafeOpenFileViewModelData cloneWithYubikeyError( - Throwable error) - { - var newData = new PasswdSafeOpenFileViewModelData(this); - newData.itsYubikeyError = error; - return newData; - } - - /** - * Clone with password retries - */ - public PasswdSafeOpenFileViewModelData cloneWithPasswordRetries(int retries) - { - var newData = new PasswdSafeOpenFileViewModelData(this); - newData.itsRetries = retries; - return newData; - } - - /** - * Clone with saved password state - */ - public PasswdSafeOpenFileViewModelData cloneWithSavedPasswordState( - SavedPasswordState state, - @Nullable CharSequence loadedMsg, - @Nullable Owner.Param loadedPassword) - { - var newData = new PasswdSafeOpenFileViewModelData(this); - newData.itsSavedPasswordState = state; - newData.itsLoadedPasswordMsg = loadedMsg; - newData.setLoadedPassword(loadedPassword); - return newData; - } - - /** - * Clone with the password to use during a file open - */ - public PasswdSafeOpenFileViewModelData cloneWithOpenPassword( - @Nullable Owner.Param password, - boolean fromYubikey) - { - var newData = new PasswdSafeOpenFileViewModelData(this); - newData.setOpenPassword(password); - newData.itsIsOpenYubikey = fromYubikey; - return newData; - } - - public boolean isResolved() - { - return itsIsResolved; - } - - @Nullable - public PasswdFileUri getUri() - { - return itsPasswdFileUri; - } - - public boolean isSaveAllowed() - { - return itsIsSaveAllowed; - } - - public boolean hasYubiInfo() - { - return itsHasYubiState; - } - - @NonNull - public YubiState getYubiState() - { - return itsYubiState; - } - - public int getYubiSlot() - { - return itsYubiSlot; - } - - public boolean isYubikeySelected() - { - return itsIsYubikeySelected; - } - - public Throwable getYubikeyError() - { - return itsYubikeyError; - } - - public boolean hasPasswordRetry() - { - return itsRetries > 0; - } - - public int getPasswordRetries() - { - return itsRetries; - } - - public SavedPasswordState getSavedPasswordState() - { - return itsSavedPasswordState; - } - - @Nullable - public CharSequence getLoadedPasswordMsg() - { - return itsLoadedPasswordMsg; - } - - @CheckResult - @Nullable - public Owner getLoadedPassword() - { - return (itsLoadedPassword != null) ? itsLoadedPassword.pass().use() : - null; - } - - @CheckResult - @Nullable - public Owner getOpenPassword() - { - return (itsOpenPassword != null) ? itsOpenPassword.pass().use() : null; - } - - public boolean isOpenYubikey() - { - return itsIsOpenYubikey; - } - - @NonNull - @SuppressLint("DefaultLocale") - public String toString() - { - return String.format("{\nuri: %s, save allowed: %b, retries: %d" + - "\nyubi state: %s, slot: %d, selected: %b, " + - "error: %s" + - "\nsaved passwd: %s, loaded passwd: %b, loaded " + - "msg: %s" + - "\nopen passwd %b, open yubikey %b}", - itsPasswdFileUri, itsIsSaveAllowed, itsRetries, - itsYubiState, itsYubiSlot, itsIsYubikeySelected, - itsYubikeyError, itsSavedPasswordState, - (itsLoadedPassword != null), itsLoadedPasswordMsg, - (itsOpenPassword != null), itsIsOpenYubikey); - } - - /** - * Close the view model data and release resources - */ - @Override - public void close() - { - setOpenPassword(null); - setLoadedPassword(null); - } - - /** - * Finalize the data - */ - protected void finalize() - { - PasswdSafeUtil.dbginfo(TAG, "data finalize"); - close(); - } - - /** - * Set the open password - */ - private void setOpenPassword(@Nullable Owner.Param password) - { - if (itsOpenPassword != null) { - itsOpenPassword.close(); - itsOpenPassword = null; - } - - if (password != null) { - itsOpenPassword = password.use(); - } - } - - /** - * Set the loaded password - */ - private void setLoadedPassword(@Nullable Owner.Param password) - { - if (itsLoadedPassword != null) { - itsLoadedPassword.close(); - itsLoadedPassword = null; - } - - if (password != null) { - itsLoadedPassword = password.use(); - } - } -} diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordBasicFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordBasicFragment.java index 49a6d2d9f..29ddfdf32 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordBasicFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordBasicFragment.java @@ -59,7 +59,6 @@ import net.tjado.bluetooth.BluetoothUtils; import net.tjado.passwdsafe.file.PasswdFileData; import net.tjado.passwdsafe.file.PasswdHistory; -import net.tjado.passwdsafe.file.PasswdRecord; import net.tjado.passwdsafe.lib.ActContext; import net.tjado.passwdsafe.lib.PasswdSafeUtil; import net.tjado.passwdsafe.lib.ObjectHolder; @@ -163,21 +162,6 @@ private enum PasswordVisibilityChange private View itsProtectedRow; private View itsReferencesRow; private ListView itsReferences; - private View itsFidoRow; - private View itsFidoRpIdRow; - private TextView itsFidoRpId; - private View itsFidoRpNameRow; - private TextView itsFidoRpName; - private View itsFidoUserHandleRow; - private TextView itsFidoUserHandle; - private View itsFidoUserNameRow; - private TextView itsFidoUserName; - private View itsFidoUserDisplayNameRow; - private TextView itsFidoUserDisplayName; - private View itsFidoUsageCounterRow; - private TextView itsFidoUsageCounter; - private View itsFidoU2fHandleRow; - private TextView itsFidoU2fHandle; private Button itsOtpAdd; private Button itsOtpAddCamera; private TokenCode itsOtp; @@ -430,23 +414,6 @@ public int getInputType() itsReferences.setOnItemClickListener( (parent, view, position, id) -> showRefRec(false, position)); - itsFidoRow = root.findViewById(R.id.fido_row); - itsFidoRpIdRow = root.findViewById(R.id.rp_id_row); - itsFidoRpId = root.findViewById(R.id.rp_id); - itsFidoRpNameRow = root.findViewById(R.id.rp_name_row); - itsFidoRpName = root.findViewById(R.id.rp_name); - itsFidoUserHandleRow = root.findViewById(R.id.user_handle_row); - itsFidoUserHandle = root.findViewById(R.id.user_handle); - itsFidoUserNameRow = root.findViewById(R.id.user_name_row); - itsFidoUserName = root.findViewById(R.id.user_name); - itsFidoUserDisplayNameRow = root.findViewById(R.id.user_displayname_row); - itsFidoUserDisplayName = root.findViewById(R.id.user_displayname); - itsFidoUsageCounterRow = root.findViewById(R.id.usage_counter_row); - itsFidoUsageCounter = root.findViewById(R.id.usage_counter); - itsFidoU2fHandleRow = root.findViewById(R.id.u2f_handle_row); - itsFidoU2fHandle = root.findViewById(R.id.u2f_handle); - - registerForContextMenu(itsUserRow); registerForContextMenu(itsPasswordRow); updatePasswordShown(PasswordVisibilityChange.INITIAL, 0, false); @@ -669,59 +636,44 @@ protected void doRefresh(@NonNull RecordInfo info) Date lastModTime = null; Integer autotypeDelimiter = null; Integer autotypeReturnSuffix = null; - // init variables for all FIDO fields - String fidoSiteId = null; - String fidoSiteName = null; - String fidoUserHandle = null; - String fidoUserName = null; - String fidoUserDisplayName = null; - Integer fidoUsageCounter = null; - String fidoU2fHandle = null; - - - if (info.itsPasswdRec.getType() == PasswdRecord.Type.NORMAL) { + switch (info.itsPasswdRec.getType()) { + case NORMAL: { itsBaseRow.setVisibility(View.GONE); - url = info.itsFileData.getURL(info.itsRec, PasswdFileData.UrlStyle.FULL); - email = info.itsFileData.getEmail(info.itsRec, PasswdFileData.EmailStyle.FULL); + url = info.itsFileData.getURL(info.itsRec, + PasswdFileData.UrlStyle.FULL); + email = info.itsFileData.getEmail(info.itsRec, + PasswdFileData.EmailStyle.FULL); otp = info.itsFileData.getOtp(info.itsRec); creationTime = info.itsFileData.getCreationTime(info.itsRec); lastModTime = info.itsFileData.getLastModTime(info.itsRec); - autotypeDelimiter = info.itsFileData.getAutotypeDelimiter(info.itsRec); - autotypeReturnSuffix = info.itsFileData.getAutotypeReturnSuffix(info.itsRec); - - // get FIDO fields - fidoSiteId = info.itsFileData.getFidoRpId(info.itsRec); - fidoSiteName = info.itsFileData.getFidoRpName(info.itsRec); - fidoUserHandle = info.itsFileData.getFidoUserHandle(info.itsRec); - fidoUserName = info.itsFileData.getFidoUserName(info.itsRec); - fidoUserDisplayName = info.itsFileData.getFidoUserDisplayName(info.itsRec); - fidoUsageCounter = info.itsFileData.getFidoKeyUseCounter(info.itsRec); - fidoU2fHandle = info.itsFileData.getFidoU2fRpId(info.itsRec); - } else if (info.itsPasswdRec.getType() == PasswdRecord.Type.ALIAS) { + autotypeDelimiter = info.itsFileData.getAutotypeDelimiter( + info.itsRec); + autotypeReturnSuffix = info.itsFileData.getAutotypeReturnSuffix( + info.itsRec); + break; + } + case ALIAS: { itsBaseRow.setVisibility(View.VISIBLE); itsBaseLabel.setText(R.string.alias_base_record_header); itsBase.setText(info.itsFileData.getId(ref)); hiddenId = R.string.hidden_password_alias; recForPassword = ref; - url = info.itsFileData.getURL(info.itsRec, PasswdFileData.UrlStyle.FULL); - email = info.itsFileData.getEmail(info.itsRec, PasswdFileData.EmailStyle.FULL); + url = info.itsFileData.getURL(info.itsRec, + PasswdFileData.UrlStyle.FULL); + email = info.itsFileData.getEmail(info.itsRec, + PasswdFileData.EmailStyle.FULL); otp = info.itsFileData.getOtp(info.itsRec); creationTime = info.itsFileData.getCreationTime(recForPassword); lastModTime = info.itsFileData.getLastModTime(recForPassword); - autotypeDelimiter = info.itsFileData.getAutotypeDelimiter(info.itsRec); - autotypeReturnSuffix = info.itsFileData.getAutotypeReturnSuffix(info.itsRec); - - // get FIDO fields - fidoSiteId = info.itsFileData.getFidoRpId(info.itsRec); - fidoSiteName = info.itsFileData.getFidoRpName(info.itsRec); - fidoUserHandle = info.itsFileData.getFidoUserHandle(info.itsRec); - fidoUserName = info.itsFileData.getFidoUserName(info.itsRec); - fidoUserDisplayName = info.itsFileData.getFidoUserDisplayName(info.itsRec); - fidoUsageCounter = info.itsFileData.getFidoKeyUseCounter(info.itsRec); - fidoU2fHandle = info.itsFileData.getFidoU2fRpId(info.itsRec); - } else if (info.itsPasswdRec.getType() == PasswdRecord.Type.SHORTCUT) { + autotypeDelimiter = info.itsFileData.getAutotypeDelimiter( + info.itsRec); + autotypeReturnSuffix = info.itsFileData.getAutotypeReturnSuffix( + info.itsRec); + break; + } + case SHORTCUT: { itsBaseRow.setVisibility(View.VISIBLE); itsBaseLabel.setText(R.string.shortcut_base_record_header); itsBase.setText(info.itsFileData.getId(ref)); @@ -729,17 +681,21 @@ protected void doRefresh(@NonNull RecordInfo info) recForPassword = ref; creationTime = info.itsFileData.getCreationTime(recForPassword); lastModTime = info.itsFileData.getLastModTime(recForPassword); + break; + } } itsTitle = info.itsFileData.getTitle(info.itsRec); - setFieldText(itsGroup, itsGroupRow, info.itsFileData.getGroup(info.itsRec)); - setFieldText(itsUser, itsUserRow, info.itsFileData.getUsername(info.itsRec)); - + setFieldText(itsGroup, itsGroupRow, + info.itsFileData.getGroup(info.itsRec)); + setFieldText(itsUser, itsUserRow, + info.itsFileData.getUsername(info.itsRec)); itsIsPasswordShown = false; itsHiddenPasswordStr = getString(hiddenId); String password = info.itsFileData.getPassword(recForPassword); - setFieldText(itsPassword, itsPasswordRow, ((password != null && password.length() > 0) ? itsHiddenPasswordStr : null)); + setFieldText(itsPassword, itsPasswordRow, + ((password != null) ? itsHiddenPasswordStr : null)); int passwordLen = (password != null) ? password.length() : 0; itsPasswordSeek.setMax(passwordLen); itsSubsetErrorStr = getString(R.string.password_subset_error, @@ -760,14 +716,6 @@ protected void doRefresh(@NonNull RecordInfo info) itsAutoTypeBluetoothUsername.setEnabled(false); } - if (itsPasswordRow.getVisibility() == View.GONE) { - itsAutoTypeUsbPassword.setEnabled(false); - itsAutoTypeBluetoothPassword.setEnabled(false); - - itsAutoTypeUsbCredentials.setEnabled(false); - itsAutoTypeBluetoothCredential.setEnabled(false); - } - if (autotypeDelimiter != null && autotypeDelimiter == 1) { itsAutoTypeDelimiter.check(R.id.autotype_delimiter_return); } @@ -776,20 +724,12 @@ protected void doRefresh(@NonNull RecordInfo info) itsAutoTypeReturnSuffix.setChecked(true); } - GuiUtils.setVisible(itsTimesRow, (creationTime != null) || (lastModTime != null)); + GuiUtils.setVisible(itsTimesRow, + (creationTime != null) || (lastModTime != null)); setFieldDate(itsCreationTime, itsCreationTimeRow, creationTime); setFieldDate(itsLastModTime, itsLastModTimeRow, lastModTime); - GuiUtils.setVisible(itsProtectedRow, info.itsFileData.isProtected(info.itsRec)); - - GuiUtils.setVisible(itsFidoRow, (fidoSiteId != null)); - setFieldText(itsFidoRpId, itsFidoRpIdRow, fidoSiteId); - setFieldText(itsFidoRpName, itsFidoRpNameRow, fidoSiteName); - setFieldText(itsFidoUserHandle, itsFidoUserHandleRow, fidoUserHandle); - setFieldText(itsFidoUserName, itsFidoUserNameRow, fidoUserName); - setFieldText(itsFidoUserDisplayName, itsFidoUserDisplayNameRow, fidoUserDisplayName); - setFieldText(itsFidoUsageCounter, itsFidoUsageCounterRow, String.valueOf((fidoUsageCounter == null) ? 0 : fidoUsageCounter)); - setFieldText(itsFidoU2fHandle, itsFidoU2fHandleRow, fidoU2fHandle); - + GuiUtils.setVisible(itsProtectedRow, + info.itsFileData.isProtected(info.itsRec)); List references = info.itsPasswdRec.getRefsToRecord(); boolean hasReferences = (references != null) && !references.isEmpty(); diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordFragment.java index 2bb035281..0b00ed1c7 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordFragment.java @@ -23,6 +23,7 @@ import com.google.android.material.tabs.TabLayout; import net.tjado.passwdsafe.file.PasswdNotes; +import net.tjado.passwdsafe.lib.view.GuiUtils; import net.tjado.passwdsafe.view.PasswdLocation; import org.pwsafe.lib.file.PwsRecord; diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordIconFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordIconFragment.java index c3630e2a0..abe96a25d 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordIconFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/PasswdSafeRecordIconFragment.java @@ -34,6 +34,7 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.appcompat.widget.SearchView; +import net.tjado.passwdsafe.file.PasswdFileData; import net.tjado.passwdsafe.lib.ObjectHolder; import net.tjado.passwdsafe.lib.PasswdSafeUtil; import net.tjado.passwdsafe.util.Pair; @@ -53,6 +54,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Collections; +import java.util.Comparator; import java.util.List; /** diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/PreferencesFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/PreferencesFragment.java index aeb826086..3cff75b59 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/PreferencesFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/PreferencesFragment.java @@ -34,6 +34,7 @@ import net.tjado.passwdsafe.file.PasswdFileDataUser; import net.tjado.passwdsafe.file.PasswdFileUri; import net.tjado.passwdsafe.file.PasswdPolicy; +import net.tjado.passwdsafe.lib.BuildConfig; import net.tjado.passwdsafe.util.AboutUtils; import net.tjado.passwdsafe.lib.ApiCompat; import net.tjado.passwdsafe.lib.ManagedRef; diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/ReleaseNotesFragment.java b/authorizer/src/main/java/net/tjado/passwdsafe/ReleaseNotesFragment.java index 821eac4a9..4197a0451 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/ReleaseNotesFragment.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/ReleaseNotesFragment.java @@ -10,11 +10,17 @@ import android.content.Context; import android.os.Bundle; import android.text.Html; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import net.tjado.passwdsafe.lib.view.GuiUtils; +import net.tjado.passwdsafe.util.AboutUtils; + +import java.util.Locale; + import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/UsbGpgBackupActivity.java b/authorizer/src/main/java/net/tjado/passwdsafe/UsbGpgBackupActivity.java index 698ca2c01..4be171be4 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/UsbGpgBackupActivity.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/UsbGpgBackupActivity.java @@ -10,13 +10,21 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.AssetFileDescriptor; +import android.database.Cursor; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import androidx.documentfile.provider.DocumentFile; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.MediaStore; import android.view.View; import android.widget.Button; import android.widget.TextView; @@ -39,11 +47,17 @@ import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.SecureRandom; import java.security.Security; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.Calendar; import java.util.Date; diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/YubikeyMgr.java b/authorizer/src/main/java/net/tjado/passwdsafe/YubikeyMgr.java index 89bd601c3..2d52b3afb 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/YubikeyMgr.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/YubikeyMgr.java @@ -7,27 +7,30 @@ */ package net.tjado.passwdsafe; +import android.annotation.SuppressLint; import android.app.Activity; +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentFilter; +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.nfc.tech.IsoDep; import android.os.CountDownTimer; import android.util.Log; +import android.widget.Toast; import androidx.annotation.CheckResult; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.UiThread; -import androidx.fragment.app.Fragment; +import net.tjado.passwdsafe.lib.ApiCompat; import net.tjado.passwdsafe.lib.PasswdSafeUtil; +import net.tjado.passwdsafe.lib.Utils; import net.tjado.passwdsafe.util.ClearingByteArrayOutputStream; -import net.tjado.passwdsafe.view.CloseableLiveData; -import com.yubico.yubikit.core.YubiKeyDevice; -import com.yubico.yubikit.yubiotp.Slot; -import com.yubico.yubikit.yubiotp.YubiOtpSession; +import net.tjado.passwdsafe.util.YubiState; import org.pwsafe.lib.Util; import org.pwsafe.lib.file.Owner; import org.pwsafe.lib.file.PwsPassword; -import java.io.Closeable; import java.io.UnsupportedEncodingException; @@ -36,26 +39,35 @@ */ public class YubikeyMgr { + /// Command to select the app running on the key + private static final byte[] SELECT_CMD = + {0x00, (byte) 0xA4, 0x04, 0x00, 0x07, + (byte) 0xA0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x00}; + /// Command to perform a hash operation + private static final byte[] HASH_CMD = {0x00, 0x01, 0x00, 0x00 }; + + private static final byte SLOT_CHAL_HMAC1 = 0x30; + private static final byte SLOT_CHAL_HMAC2 = 0x38; + private static final int SHA1_MAX_BLOCK_SIZE = 64; + private static final boolean TEST = false;//PasswdSafeUtil.DEBUG; + private static final String TAG = "YubikeyMgr"; - private final YubikeyViewModel itsYubikeyModel; private User itsUser = null; + private boolean itsIsRegistered = false; + private PendingIntent itsTagIntent = null; private CountDownTimer itsTimer = null; - private final CloseableLiveData itsResult = - new CloseableLiveData<>(); /// Interface for a user of the YubikeyMgr public interface User { /// Get the activity using the key - @NonNull Activity getActivity(); /// Get the password to be sent to the key - @CheckResult - @Nullable + @CheckResult @Nullable Owner getUserPassword(); /// Get the slot number to use on the key @@ -69,37 +81,92 @@ void timerTick(@SuppressWarnings("SameParameterValue") int totalTime, int remainingTime); } - /** - * Constructor - */ - public YubikeyMgr(@NonNull YubikeyViewModel yubikeyModel, - @NonNull Fragment openFrag) + /** Get the state of support for the Yubikey */ + public YubiState getState(Activity act) { - itsYubikeyModel = yubikeyModel; - var fragLifecycleOwner = openFrag.getViewLifecycleOwner(); - itsResult.observe(fragLifecycleOwner, this::onYubikeyResultChanged); - itsYubikeyModel.getDeviceData().observe(fragLifecycleOwner, - this::onYubikeyDeviceChanged); + if (TEST) { + return YubiState.ENABLED; + } + + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(act); + if (adapter == null) { + return YubiState.UNAVAILABLE; + } else if (!adapter.isEnabled()) { + return YubiState.DISABLED; + } + return YubiState.ENABLED; } - /** - * Start the interaction with the YubiKey - */ - public void start(@NonNull User user) + /// Start the interaction with the YubiKey + @SuppressLint("UnspecifiedImmutableFlag") + public void start(User user) { if (itsUser != null) { stop(); } + itsUser = user; + Activity act = itsUser.getActivity(); + if (itsTagIntent == null) { + Intent intent = new Intent(act, act.getClass()); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + itsTagIntent = PendingIntent.getActivity( + act, 0, intent, ApiCompat.getPendingIntentMutableFlag()); + } - if (YubikeyViewModel.TEST) { - testYubikey(); - } else { - startYubikey(); + if (TEST) { + new CountDownTimer(5000, 5000) { + @Override + public void onTick(long millisUntilFinished) + { + } + + @Override + public void onFinish() + { + if (itsUser != null) { + try (Owner password = + itsUser.getUserPassword()) { + if (password == null) { + stopUser(null, null); + return; + } + String utf8 = "UTF-8"; + byte[] bytes = password.get().getBytes(utf8); + String passwordStr = + new String(bytes, utf8).toLowerCase(); + try (Owner newPassword = + PwsPassword.create(passwordStr)) { + stopUser(newPassword.pass(), null); + } + } catch (UnsupportedEncodingException e) { + Log.e(TAG, "encode error", e); + } + } + } + }.start(); + } else if (!itsIsRegistered) { + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(act); + if (adapter == null) { + Toast.makeText(act, "NO NFC", Toast.LENGTH_LONG).show(); + return; + } + + if (!adapter.isEnabled()) { + Toast.makeText(act, "NFC DISABLED", Toast.LENGTH_LONG).show(); + return; + } + + IntentFilter iso = + new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED); + adapter.enableForegroundDispatch( + act, itsTagIntent, new IntentFilter[] { iso }, + new String[][] + { new String[] { IsoDep.class.getName() } }); + itsIsRegistered = true; } - itsTimer = new CountDownTimer(YubikeyViewModel.KEY_TIMEOUT, 1 * 1000) - { + itsTimer = new CountDownTimer(30 * 1000, 1 * 1000) { @Override public void onFinish() { @@ -109,73 +176,80 @@ public void onFinish() @Override public void onTick(long millisUntilFinished) { - itsUser.timerTick(YubikeyViewModel.KEY_TIMEOUT / 1000, - (int)(millisUntilFinished / 1000)); + itsUser.timerTick(30, (int)(millisUntilFinished / 1000)); } }; itsTimer.start(); } - /** - * Stop the interaction with the key - */ + /** Handle a pause of the activity */ + public void onPause() + { + if (itsUser == null) { + return; + } + Activity act = itsUser.getActivity(); + + if (itsIsRegistered) { + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(act); + if ((adapter == null) || !adapter.isEnabled()) { + return; + } + + adapter.disableForegroundDispatch(act); + itsIsRegistered = false; + } + + if (itsTagIntent != null) { + itsTagIntent.cancel(); + itsTagIntent = null; + } + } + + /// Stop the interaction with the key public void stop() { onPause(); stopUser(null, null); - itsResult.close(); itsTimer = null; itsUser = null; } - /** - * Handle a pause of the using fragment - */ - public void onPause() - { - if (itsUser != null) { - itsYubikeyModel.stopNfc(itsUser.getActivity()); - } - } - - /** - * Use a discovered YubiKey - */ - @UiThread - private void useYubikey(YubiKeyDevice device) + /// Handle the intent for when the key is discovered + public void handleKeyIntent(Intent intent) { - PasswdSafeUtil.dbginfo(TAG, "Use YubiKey %s, has user: %b", device, - (itsUser != null)); - if (itsUser == null) { + if (!NfcAdapter.ACTION_TECH_DISCOVERED.equals(intent.getAction())) { return; } - try (var userPassword = itsUser.getUserPassword()) { - doUseYubikey(device, - (userPassword != null) ? userPassword.pass() : null, - itsUser.getSlotNum(), itsResult); + PasswdSafeUtil.dbginfo(TAG, "calculate"); + Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); + if ((tag == null) || (itsUser == null)) { + return; } - } - /** - * Implementation to use a discovered YubiKey - */ - @UiThread - private static void doUseYubikey(final YubiKeyDevice device, - @Nullable - final Owner.Param password, - final int slotNum, - final CloseableLiveData result) - { - YubiOtpSession.create(device, sessionResult -> { - try (var userPassword = (password != null) ? password.use() : null; - var pwbytes = new ClearingByteArrayOutputStream()) { - YubiOtpSession otp = sessionResult.getValue(); + IsoDep isotag = IsoDep.get(tag); + try { + isotag.connect(); + ClearingByteArrayOutputStream cmd = + new ClearingByteArrayOutputStream(); + byte[] resp = null; + try (Owner userPassword = itsUser.getUserPassword()) { if (userPassword == null) { throw new Exception("No password"); } + + resp = isotag.transceive(SELECT_CMD); + checkResponse(resp); + Util.clearArray(resp); + PwsPassword pw = userPassword.get(); + cmd.write(HASH_CMD); + + // Placeholder for length + byte datalen; + cmd.write(0); int pwlen = pw.length(); if (pwlen > 0) { @@ -184,122 +258,77 @@ private static void doUseYubikey(final YubiKeyDevice device, } // Chars are encoded as little-endian UTF-16. A trailing // zero must be skipped as the PC API will skip it. + datalen = 0; for (int i = 0; i < pwlen - 1; ++i) { + datalen += 2; char c = pw.charAt(i); - pwbytes.write(c & 0xff); - pwbytes.write((c >> 8) & 0xff); + cmd.write(c & 0xff); + cmd.write((c >> 8) & 0xff); } char c = pw.charAt(pwlen - 1); - pwbytes.write(c & 0xff); + cmd.write(c & 0xff); + ++datalen; int last = (c >> 8) & 0xff; if (last != 0) { - pwbytes.write(last); + cmd.write(last); + ++datalen; } } else { // Empty password needs a single null byte - pwbytes.write(0); + datalen = 1; + cmd.write(0); } - byte[] resp = otp.calculateHmacSha1( - slotNum == 1 ? Slot.ONE : Slot.TWO, - pwbytes.toByteArray(), null); + byte[] cmdbytes = cmd.toByteArray(); try { - // Prune response bytes and convert - char[] pwstr = Util.bytesToHexChars(resp, 0, resp.length); - try (Owner newPassword = PwsPassword.create( - pwstr)) { - result.postValue( - new KeyResult(newPassword.pass(), null)); + int slot = itsUser.getSlotNum(); + if (slot == 1) { + cmdbytes[2] = SLOT_CHAL_HMAC1; + } else { + cmdbytes[2] = SLOT_CHAL_HMAC2; } + cmdbytes[HASH_CMD.length] = datalen; + resp = isotag.transceive(cmdbytes); + checkResponse(resp); } finally { - Util.clearArray(resp); + Util.clearArray(cmdbytes); } - } catch (Exception e) { - PasswdSafeUtil.dbginfo(TAG, e, "Error creating OTP session"); - result.postValue(new KeyResult(null, e)); - } - }); - } - /** - * Start using the YubiKey - */ - @UiThread - private void startYubikey() - { - var yubikeyDevice = itsYubikeyModel.getDeviceData().getValue(); - if (yubikeyDevice != null) { - useYubikey(yubikeyDevice); - } else { - itsYubikeyModel.startNfc(itsUser.getActivity()); - } - } + // Prune response bytes and convert - /** - * Test using the YubiKey - */ - @UiThread - private void testYubikey() - { - new CountDownTimer(5000, 5000) - { - @Override - public void onTick(long millisUntilFinished) - { - } - - @Override - public void onFinish() - { - if (itsUser == null) { - return; + char[] pwstr = Util.bytesToHexChars(resp, 0, resp.length - 2); + try (Owner newPassword = + PwsPassword.create(pwstr)) { + stopUser(newPassword.pass(), null); + } finally { + Util.clearArray(resp); } - try (var password = itsUser.getUserPassword()) { - if (password == null) { - itsResult.postValue(new KeyResult(null, null)); - return; - } - String utf8 = "UTF-8"; - byte[] bytes = password.get().getBytes(utf8); - String passwordStr = new String(bytes, utf8).toLowerCase(); - try (Owner newPassword = PwsPassword.create( - passwordStr)) { - itsResult.postValue( - new KeyResult(newPassword.pass(), null)); - } - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "encode error", e); - itsResult.postValue(new KeyResult(null, e)); + } finally { + if (resp != null) { + Util.clearArray(resp); } + Utils.closeStreams(cmd, isotag); + Runtime.getRuntime().gc(); } - }.start(); - } - - /** - * Handle a change notification for the YubiKey device - */ - private void onYubikeyDeviceChanged(YubiKeyDevice device) - { - PasswdSafeUtil.dbginfo(TAG, "YubiDevice changed: %s", - YubikeyViewModel.toString(device)); - if ((itsUser != null) && (device != null)) { - useYubikey(device); + } catch (Exception e) { + PasswdSafeUtil.dbginfo(TAG, e, "handleKeyIntent"); + stopUser(null, e); } + } - /** - * Handle a change notification for the result of using the YubiKey - */ - private void onYubikeyResultChanged(KeyResult result) + /// Check for a valid response + private static void checkResponse(byte[] resp) throws Exception { - try (result) { - if (result != null) { - stopUser( - ((result.itsPassword != null) ? - result.itsPassword.pass() : null), result.itsError); - } + if ((resp.length >= 2) && + (resp[resp.length - 2] == (byte)0x90) && + (resp[resp.length - 1] == 0x00)) { + return; } + + throw new Exception("Invalid response: " + + Util.bytesToHex(resp)); } /** @@ -314,30 +343,4 @@ private void stopUser(Owner.Param password, Exception e) itsUser.finish(password, e); } } - - /** - * Result of using the YubiKey to calculate the password - */ - private static class KeyResult implements Closeable - { - protected final Owner itsPassword; - protected final Exception itsError; - - /** - * Constructor - */ - protected KeyResult(Owner.Param password, Exception error) - { - itsPassword = (password != null) ? password.use() : null; - itsError = error; - } - - @Override - public void close() - { - if (itsPassword != null) { - itsPassword.close(); - } - } - } -} \ No newline at end of file +} diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/YubikeyViewModel.java b/authorizer/src/main/java/net/tjado/passwdsafe/YubikeyViewModel.java deleted file mode 100644 index ea9a2e033..000000000 --- a/authorizer/src/main/java/net/tjado/passwdsafe/YubikeyViewModel.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright (©) 2023 Jeff Harris - * All rights reserved. Use of the code is allowed under the - * Artistic License 2.0 terms, as specified in the LICENSE file - * distributed with this code, or available from - * http://www.opensource.org/licenses/artistic-license-2.0.php - */ -package net.tjado.passwdsafe; - -import android.app.Activity; -import android.app.Application; -import android.content.Context; -import android.content.pm.PackageManager; -import android.nfc.NfcAdapter; -import android.os.Handler; -import android.os.Looper; -import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; - -import net.tjado.passwdsafe.lib.ManagedRef; -import net.tjado.passwdsafe.lib.PasswdSafeUtil; -import net.tjado.passwdsafe.util.YubiState; -import com.yubico.yubikit.android.YubiKitManager; -import com.yubico.yubikit.android.transport.nfc.NfcConfiguration; -import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable; -import com.yubico.yubikit.android.transport.usb.UsbConfiguration; -import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice; -import com.yubico.yubikit.core.YubiKeyDevice; - -/** - * View model for a YubiKey - */ -public class YubikeyViewModel extends AndroidViewModel -{ - public static final int KEY_TIMEOUT = 30 * 1000; - - // Also decide proper log level in logback.xml - public static final boolean TEST = false;//PasswdSafeUtil.DEBUG; - - private static final long NFC_STOP_DELAY = 5 * 1000; - - private static final String TAG = "YubikeyViewModel"; - - /** - * NFC state - */ - private enum NfcState - { - UNAVAILABLE, - DISABLED, - ENABLED - } - - private final YubiKitManager itsYubiMgr; - private NfcState itsNfcState; - private final boolean itsHasUsb; - private final Handler itsUiHandler = new Handler(Looper.getMainLooper()); - private ManagedRef itsStopAct; - private final MutableLiveData itsYubiDevice = - new MutableLiveData<>(); - - /** - * Constructor - */ - public YubikeyViewModel(@NonNull Application app) - { - super(app); - itsYubiMgr = new YubiKitManager(app); - itsNfcState = getNfcState(app); - - var pkgmgr = app.getPackageManager(); - itsHasUsb = pkgmgr.hasSystemFeature(PackageManager.FEATURE_USB_HOST); - if (itsHasUsb) { - itsYubiMgr.startUsbDiscovery(new UsbConfiguration(), device -> { - PasswdSafeUtil.dbginfo(TAG, "USB discovery, device: %s", - toString(device)); - if (!device.hasPermission()) { - return; - } - - device.setOnClosed(() -> { - PasswdSafeUtil.dbginfo(TAG, "USB device removed"); - itsYubiDevice.postValue(null); - }); - - itsYubiDevice.postValue(device); - }); - } - } - - /** - * Get the state of support for the YubiKey - */ - public YubiState getState(Context ctx) - { - itsNfcState = getNfcState(ctx); - - if (TEST) { - return YubiState.ENABLED; - } - - switch (itsNfcState) { - case UNAVAILABLE: { - return itsHasUsb ? YubiState.USB_ENABLED_NFC_UNAVAILABLE : - YubiState.UNAVAILABLE; - } - case DISABLED: { - return itsHasUsb ? YubiState.USB_ENABLED_NFC_DISABLED : - YubiState.USB_DISABLED_NFC_DISABLED; - } - case ENABLED: { - return itsHasUsb ? YubiState.ENABLED : - YubiState.USB_DISABLED_NFC_ENABLED; - } - } - return YubiState.UNAVAILABLE; - } - - /** - * Get the live YubiKey device - */ - @NonNull - public LiveData getDeviceData() - { - return itsYubiDevice; - } - - /** - * Is the current YubiKey device a USB device - */ - public boolean isUsbYubikeyDevice() - { - return isUsbYubikey(itsYubiDevice.getValue()); - } - - /** - * Start using NFC to discover a YubiKey - */ - public void startNfc(@NonNull Activity act) - { - if (isNfcEnabled()) { - try { - itsYubiMgr.startNfcDiscovery( - new NfcConfiguration().timeout(KEY_TIMEOUT), act, - device -> { - PasswdSafeUtil.dbginfo(TAG, - "NFC discover, device: %s", - toString(device)); - - itsUiHandler.post(() -> { - itsYubiDevice.setValue(device); - itsYubiDevice.postValue(null); - }); - }); - } catch (NfcNotAvailable e) { - PasswdSafeUtil.dbginfo(TAG, e, "NFC discovery failed"); - } - } - } - - /** - * Stop using NFC to discover a YubiKey - */ - public void stopNfc(@NonNull Activity act) - { - if (isNfcEnabled()) { - // Delay stopping NFC for a few seconds to allow the user time to - // move the key away from the device. Otherwise, the key may - // activate a second time and load the default activity. - if (itsStopAct != null) { - itsStopAct.clear(); - } - itsStopAct = new ManagedRef<>(act); - - itsUiHandler.postDelayed(() -> { - PasswdSafeUtil.dbginfo(TAG, "stopNfc stopping"); - if (itsStopAct != null) { - var stopAct = itsStopAct.get(); - if (stopAct != null) { - itsYubiMgr.stopNfcDiscovery(stopAct); - itsStopAct.clear(); - } - } - }, NFC_STOP_DELAY); - } - } - - /** - * Is a YubiKey device a USB YubiKey device - */ - public static boolean isUsbYubikey(YubiKeyDevice device) - { - return (device instanceof UsbYubiKeyDevice); - } - - /** - * Get a string identifier for a YubiKey device - */ - public static String toString(YubiKeyDevice device) - { - if (device == null) { - return "(null)"; - } else if (device instanceof UsbYubiKeyDevice) { - // The default USB device toString is too verbose - var usbdevice = (UsbYubiKeyDevice)device; - var dev = usbdevice.getUsbDevice(); - var pid = usbdevice.getPid(); - return "UsbYubiKeyDevice{" + "usbDevice=[UsbDevice[name=" + - dev.getDeviceName() + ", vendor=" + dev.getVendorId() + - ", product=" + dev.getProductId() + "], usbPid=" + pid + '}'; - } - return device.toString(); - } - - @Override - protected void onCleared() - { - PasswdSafeUtil.dbginfo(TAG, "onCleared"); - itsYubiDevice.setValue(null); - if (itsHasUsb) { - itsYubiMgr.stopUsbDiscovery(); - } - if (itsStopAct != null) { - itsStopAct.clear(); - } - } - - /** - * Is NFC enabled - */ - private boolean isNfcEnabled() - { - switch (itsNfcState) { - case ENABLED: { - return true; - } - case UNAVAILABLE: - case DISABLED: { - break; - } - } - return false; - } - - /** - * Get the NFC state - */ - private static NfcState getNfcState(Context ctx) - { - NfcAdapter adapter = NfcAdapter.getDefaultAdapter(ctx); - if (adapter != null) { - return adapter.isEnabled() ? NfcState.ENABLED : NfcState.DISABLED; - } else { - return NfcState.UNAVAILABLE; - } - } -} diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/file/PasswdFileData.java b/authorizer/src/main/java/net/tjado/passwdsafe/file/PasswdFileData.java index 29c2dc4a5..8c4c489fc 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/file/PasswdFileData.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/file/PasswdFileData.java @@ -1298,76 +1298,70 @@ private void setField(Object val, PwsRecord rec, int fieldId, { switch (fieldId) { - case PwsRecordV3.EMAIL: - case PwsRecordV3.GROUP: - case PwsRecordV3.NOTES: - case PwsRecordV3.TITLE: - case PwsRecordV3.URL: - case PwsRecordV3.ICON: - case PwsRecordV3.OTP: - case PwsRecordV3.FIDO_RP_ID: - case PwsRecordV3.FIDO_RP_NAME: - case PwsRecordV3.FIDO_USER_HANDLE: - case PwsRecordV3.FIDO_USER_NAME: - case PwsRecordV3.FIDO_USER_DISPLAYNAME: - case PwsRecordV3.FIDO_U2F_RP_ID: - case PwsRecordV3.FIDO_KEY_PAIR: - case PwsRecordV3.FIDO_HMAC_SECRET: - case PwsRecordV3.USERNAME: - case PwsRecordV3.PASSWORD_HISTORY: - case PwsRecordV3.PASSWORD_POLICY: - case PwsRecordV3.OWN_PASSWORD_SYMBOLS: - case PwsRecordV3.PASSWORD_POLICY_NAME: - { - String str = (val == null) ? null : val.toString(); - if (!TextUtils.isEmpty(str)) { - field = new PwsStringUnicodeField(fieldId, str); - } - break; - } - case PwsRecordV3.PASSWORD: - { - String str = (val == null) ? null : val.toString(); - if (!TextUtils.isEmpty(str)) { - field = new PwsPasswdUnicodeField(fieldId, str, itsPwsFile); - } - break; - } - case PwsRecordV3.PROTECTED_ENTRY: { - Byte b = (Byte)val; - if ((b != null) && (b != 0)) { - field = new PwsByteField(fieldId, b); - } - break; + case PwsRecordV3.EMAIL: + case PwsRecordV3.GROUP: + case PwsRecordV3.NOTES: + case PwsRecordV3.TITLE: + case PwsRecordV3.URL: + case PwsRecordV3.ICON: + case PwsRecordV3.OTP: + case PwsRecordV3.FIDO_RP_ID: + case PwsRecordV3.FIDO_RP_NAME: + case PwsRecordV3.FIDO_USER_HANDLE: + case PwsRecordV3.FIDO_USER_NAME: + case PwsRecordV3.FIDO_USER_DISPLAYNAME: + case PwsRecordV3.FIDO_U2F_RP_ID: + case PwsRecordV3.FIDO_KEY_PAIR: + case PwsRecordV3.FIDO_HMAC_SECRET: + case PwsRecordV3.USERNAME: + case PwsRecordV3.PASSWORD_HISTORY: + case PwsRecordV3.PASSWORD_POLICY: + case PwsRecordV3.OWN_PASSWORD_SYMBOLS: + case PwsRecordV3.PASSWORD_POLICY_NAME: + { + String str = (val == null) ? null : val.toString(); + if (!TextUtils.isEmpty(str)) { + field = new PwsStringUnicodeField(fieldId, str); } - case PwsRecordV3.PASSWORD_LIFETIME: { - Date d = (Date)val; - if ((d != null) && (d.getTime() != 0)) { - field = new PwsTimeField(fieldId, d); - } - break; + break; + } + case PwsRecordV3.PASSWORD: + { + String str = (val == null) ? null : val.toString(); + if (!TextUtils.isEmpty(str)) { + field = new PwsPasswdUnicodeField(fieldId, str, itsPwsFile); } - case PwsRecordV3.AUTOTYPE_DELIMITER: - case PwsRecordV3.AUTOTYPE_RETURNSUFFIX: - case PwsRecordV3.PASSWORD_EXPIRY_INTERVAL:{ - Integer ival = (Integer)val; - if ((ival != null) && (ival != 0)) { - field = new PwsIntegerField(fieldId, ival); - } - break; + break; + } + case PwsRecordV3.PROTECTED_ENTRY: { + Byte b = (Byte)val; + if ((b != null) && (b != 0)) { + field = new PwsByteField(fieldId, b); } - case PwsRecordV3.FIDO_KEY_USE_COUNTER:{ - Integer ival = (Integer)val; - if (ival != null) { - field = new PwsIntegerField(fieldId, ival); - } - break; + break; + } + case PwsRecordV3.PASSWORD_LIFETIME: { + Date d = (Date)val; + if ((d != null) && (d.getTime() != 0)) { + field = new PwsTimeField(fieldId, d); } - default: - { - fieldId = FIELD_UNSUPPORTED; - break; + break; + } + case PwsRecordV3.AUTOTYPE_DELIMITER: + case PwsRecordV3.AUTOTYPE_RETURNSUFFIX: + case PwsRecordV3.PASSWORD_EXPIRY_INTERVAL: + case PwsRecordV3.FIDO_KEY_USE_COUNTER:{ + Integer ival = (Integer)val; + if ((ival != null) && (ival != 0)) { + field = new PwsIntegerField(fieldId, ival); } + break; + } + default: + { + fieldId = FIELD_UNSUPPORTED; + break; + } } break; } diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/file/PasswdFileUri.java b/authorizer/src/main/java/net/tjado/passwdsafe/file/PasswdFileUri.java index 27b9aa57c..3c11ce0fd 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/file/PasswdFileUri.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/file/PasswdFileUri.java @@ -8,6 +8,7 @@ package net.tjado.passwdsafe.file; import android.content.ContentResolver; +import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -16,6 +17,7 @@ import android.net.Uri; import android.os.Environment; import android.provider.OpenableColumns; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.EnvironmentCompat; @@ -28,6 +30,7 @@ import net.tjado.passwdsafe.lib.ApiCompat; import net.tjado.passwdsafe.lib.DocumentsContractCompat; import net.tjado.passwdsafe.lib.PasswdSafeContract; +import net.tjado.passwdsafe.lib.ProviderType; import net.tjado.passwdsafe.lib.Utils; import net.tjado.passwdsafe.util.Pair; @@ -62,6 +65,7 @@ public class PasswdFileUri private String itsTitle = null; private Pair itsWritableInfo; private boolean itsIsDeletable; + private ProviderType itsSyncType = null; /** The type of URI */ public enum Type @@ -164,6 +168,12 @@ private PasswdFileUri(Uri uri, Context ctx) resolveGenericProviderUri(ctx); return; } + case SYNC_PROVIDER: { + itsFile = null; + itsBackupFile = null; + resolveSyncProviderUri(ctx); + return; + } case BACKUP: { itsFile = null; itsBackupFile = resolveBackupUri(ctx); @@ -383,16 +393,19 @@ public void delete(Context context) public boolean exists() { switch (itsType) { - case FILE: { - return (itsFile != null) && itsFile.exists(); - } - case EMAIL: - case GENERIC_PROVIDER: { - return true; - } - case BACKUP: { - return (itsBackupFile != null); - } + case FILE: { + return (itsFile != null) && itsFile.exists(); + } + case SYNC_PROVIDER: { + return (itsSyncType != null); + } + case EMAIL: + case GENERIC_PROVIDER: { + return true; + } + case BACKUP: { + return (itsBackupFile != null); + } } return false; } @@ -425,6 +438,12 @@ public Type getType() } + /** Get the sync type of the URI */ + public ProviderType getSyncType() + { + return itsSyncType; + } + /** Get the backup file */ public BackupFile getBackupFile() { @@ -468,6 +487,13 @@ public String getIdentifier(Context context, boolean shortId) return itsUri.getPath(); } } + case SYNC_PROVIDER: { + if (itsSyncType != null) { + return String.format("%s - %s", + itsSyncType.getName(context), itsTitle); + } + return context.getString(R.string.unknown_sync_file); + } case EMAIL: { return context.getString(R.string.email_attachment); } @@ -663,6 +689,89 @@ private Pair resolveGenericProviderFlags(Cursor cursor, return new Pair<>(true, true); } + + /** Resolve fields for a sync provider URI */ + private void resolveSyncProviderUri(Context context) + { + itsWritableInfo = new Pair<>(true, null); + itsIsDeletable = true; + if (itsSyncType != null) { + return; + } + + long providerId = -1; + boolean isFile = false; + switch (PasswdSafeContract.MATCHER.match(itsUri)) { + case PasswdSafeContract.MATCH_PROVIDER: + case PasswdSafeContract.MATCH_PROVIDER_FILES: { + providerId = Long.parseLong(itsUri.getPathSegments().get(1)); + break; + } + case PasswdSafeContract.MATCH_PROVIDER_FILE: { + providerId = Long.parseLong(itsUri.getPathSegments().get(1)); + isFile = true; + break; + } + } + + if (providerId != -1) { + ContentResolver cr = context.getContentResolver(); + resolveSyncProvider(providerId, cr); + if (isFile) { + resolveSyncFile(cr); + } + } + } + + + /** Resolve sync provider information */ + private void resolveSyncProvider(long providerId, + ContentResolver cr) + { + Uri providerUri = ContentUris.withAppendedId( + PasswdSafeContract.Providers.CONTENT_URI, providerId); + Cursor providerCursor = cr.query( + providerUri, + PasswdSafeContract.Providers.PROJECTION, + null, null, null); + try { + if ((providerCursor != null) && providerCursor.moveToFirst()) { + String typeStr = providerCursor.getString( + PasswdSafeContract.Providers.PROJECTION_IDX_TYPE); + try { + itsSyncType = ProviderType.valueOf(typeStr); + itsTitle = providerCursor.getString( + PasswdSafeContract.Providers.PROJECTION_IDX_ACCT); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Unknown provider type: " + typeStr); + } + } + } finally { + if (providerCursor != null) { + providerCursor.close(); + } + } + } + + + /** Resolve sync file information */ + private void resolveSyncFile(ContentResolver cr) + { + Cursor fileCursor = cr.query(itsUri, + PasswdSafeContract.Files.PROJECTION, + null, null, null); + try { + if ((fileCursor != null) && fileCursor.moveToFirst()) { + itsTitle = fileCursor.getString( + PasswdSafeContract.Files.PROJECTION_IDX_TITLE); + } + } finally { + if (fileCursor != null) { + fileCursor.close(); + } + } + } + /** * Resolve fields for a backup file URI */ diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/util/CountedBool.java b/authorizer/src/main/java/net/tjado/passwdsafe/util/CountedBool.java index f3fc36ecf..a23dfe80e 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/util/CountedBool.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/util/CountedBool.java @@ -47,12 +47,4 @@ public boolean get() { return (itsCount > 0); } - - /** - * Reset to the initial false value - */ - public void reset() - { - itsCount = 0; - } } diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/util/SavedPasswordState.java b/authorizer/src/main/java/net/tjado/passwdsafe/util/SavedPasswordState.java deleted file mode 100644 index d6749b848..000000000 --- a/authorizer/src/main/java/net/tjado/passwdsafe/util/SavedPasswordState.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (©) 2023 Jeff Harris - * All rights reserved. Use of the code is allowed under the - * Artistic License 2.0 terms, as specified in the LICENSE file - * distributed with this code, or available from - * http://www.opensource.org/licenses/artistic-license-2.0.php - */ -package net.tjado.passwdsafe.util; - -public enum SavedPasswordState -{ - UNKNOWN, - NOT_AVAILABLE, - AVAILABLE, - LOADED_SUCCESS, - LOADED_FAILURE -} \ No newline at end of file diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/util/YubiState.java b/authorizer/src/main/java/net/tjado/passwdsafe/util/YubiState.java index da735edfa..0b194a4ad 100644 --- a/authorizer/src/main/java/net/tjado/passwdsafe/util/YubiState.java +++ b/authorizer/src/main/java/net/tjado/passwdsafe/util/YubiState.java @@ -12,63 +12,8 @@ */ public enum YubiState { - /// State is not known UNKNOWN, - /// USB and NFC are both unavailable on the device UNAVAILABLE, - - /// USB is not available, NFC is enabled - USB_DISABLED_NFC_ENABLED, - /// USB is not available, NFC is disabled - USB_DISABLED_NFC_DISABLED, - - /// USB is available, NFC is unavailable - USB_ENABLED_NFC_UNAVAILABLE, - /// USB is available, NFC is disabled - USB_ENABLED_NFC_DISABLED, - - /// Both USB and NFC are available - ENABLED; - - /** - * Are either USB or NFC enabled - */ - public boolean isEnabled() - { - switch (this) { - case USB_DISABLED_NFC_ENABLED: - case USB_ENABLED_NFC_UNAVAILABLE: - case USB_ENABLED_NFC_DISABLED: - case ENABLED: { - return true; - } - case UNKNOWN: - case UNAVAILABLE: - case USB_DISABLED_NFC_DISABLED: { - return false; - } - } - return false; - } - - /** - * Is USB enabled - */ - public boolean isUsbEnabled() - { - switch (this) { - case USB_ENABLED_NFC_UNAVAILABLE: - case USB_ENABLED_NFC_DISABLED: - case ENABLED: { - return true; - } - case UNKNOWN: - case UNAVAILABLE: - case USB_DISABLED_NFC_ENABLED: - case USB_DISABLED_NFC_DISABLED: { - return false; - } - } - return false; - } -} \ No newline at end of file + DISABLED, + ENABLED +} diff --git a/authorizer/src/main/java/net/tjado/passwdsafe/view/CloseableLiveData.java b/authorizer/src/main/java/net/tjado/passwdsafe/view/CloseableLiveData.java deleted file mode 100644 index 17082376c..000000000 --- a/authorizer/src/main/java/net/tjado/passwdsafe/view/CloseableLiveData.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (©) 2023 Jeff Harris - * All rights reserved. Use of the code is allowed under the - * Artistic License 2.0 terms, as specified in the LICENSE file - * distributed with this code, or available from - * http://www.opensource.org/licenses/artistic-license-2.0.php - */ - -package net.tjado.passwdsafe.view; - -import androidx.lifecycle.MutableLiveData; - -import net.tjado.passwdsafe.lib.PasswdSafeUtil; - -import java.io.Closeable; -import java.io.IOException; - -/** - * The CloseableLiveData class extends MutableLiveData to attempt to close - * the data it contains. - */ -public class CloseableLiveData extends MutableLiveData - implements Closeable -{ - private static final String TAG = "CloseableLiveData"; - - /** - * Default Constructor - */ - public CloseableLiveData() - { - } - - /** - * Close the data - */ - @Override - public void close() - { - T value = getValue(); - if (value != null) { - PasswdSafeUtil.dbginfo(TAG, "Closing value"); - try { - value.close(); - } catch (IOException e) { - PasswdSafeUtil.dbginfo(TAG, e, "Error closing live data"); - } - setValue(null); - } - } - - @Override - protected void onInactive() - { - super.onInactive(); - close(); - } - - /** - * Finalize the object - */ - @Override - protected void finalize() throws Throwable - { - super.finalize(); - close(); - } -} \ No newline at end of file diff --git a/authorizer/src/main/java/net/tjado/webauthn/Authenticator.java b/authorizer/src/main/java/net/tjado/webauthn/Authenticator.java index 6d0462a5e..de7367bd9 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/Authenticator.java +++ b/authorizer/src/main/java/net/tjado/webauthn/Authenticator.java @@ -42,7 +42,6 @@ import co.nstant.in.cbor.model.UnicodeString; import net.tjado.passwdsafe.R; -import net.tjado.passwdsafe.lib.Utils; import net.tjado.webauthn.exceptions.ApduException; import net.tjado.webauthn.exceptions.ApduException.StatusWord; import net.tjado.webauthn.exceptions.CtapException; @@ -64,6 +63,7 @@ import net.tjado.webauthn.models.ICredentialSafe; import net.tjado.webauthn.models.MakeCredentialOptions; import net.tjado.webauthn.models.NoneAttestation; +import net.tjado.webauthn.models.PackedBasicAttestation; import net.tjado.webauthn.models.PackedSelfAttestation; import net.tjado.webauthn.models.PublicKeyCredentialDescriptor; import net.tjado.webauthn.models.PublicKeyCredentialSource; @@ -189,7 +189,7 @@ public AttestationObject makeCredential(MakeCredentialOptions options, FragmentA if (existingCredentialSource != null && existingCredentialSource.rpId.equals(options.rpEntity.id) && PublicKeyCredentialSource.type.equals(descriptor.type)) { - showToast(activity, "Already registered - Registration canceled!", Toast.LENGTH_SHORT); + showDialog(activity, "This authenticator is excluded!", ""); throw new CtapException(CtapError.CREDENTIAL_EXCLUDED); } @@ -228,8 +228,7 @@ public AttestationObject makeCredential(MakeCredentialOptions options, FragmentA // 8. Obtain user consent for creating a new credential // if we need to obtain user verification, create a biometric prompt for that // else just generate a new credential/attestation object - boolean permission; - /* + /*boolean permission; if (options.requireUserVerification) { String subtitle = ""; if (options.userEntity.name != null) { @@ -250,17 +249,18 @@ public AttestationObject makeCredential(MakeCredentialOptions options, FragmentA activity.getString(R.string.credentials_makeTitle, options.rpEntity.id), subtitle, cryptoObject); } else {*/ - permission = showDialog(activity, + //permission = showDialog(activity, + showDialog(activity, activity.getString(R.string.credentials_makeTitle, options.rpEntity.id), activity.getString(R.string.credentials_makeSubtitle, options.userEntity.name, options.userEntity.displayName, options.rpEntity.id, options.rpEntity.name)); //} - if (!permission) { + /*if (!permission) { // TODO: Check why using the original credentialSource _does not_ delete the credential - //credentialSource = credentialSafe.getCredentialSourceById(credentialSource.id); - //credentialSafe.deleteCredential(credentialSource); + credentialSource = credentialSafe.getCredentialSourceById(credentialSource.id); + credentialSafe.deleteCredential(credentialSource); throw new CtapException(CtapError.OPERATION_DENIED); - } + }*/ // 9. Generate a new credential boolean genHmacSec = extensionOutput.getKeys().contains(new UnicodeString("hmac-secret")); @@ -378,15 +378,12 @@ public GetAssertionResult getAssertion(GetAssertionOptions options, CredentialSe // 0. Check if all supplied parameters are well-formed options.areWellFormed(); boolean preFlight = !options.requireUserPresence; - Log.d(TAG, "getAssertion - User Presence: " + options.requireUserPresence); - Log.d(TAG, "getAssertion - User Verification: " + options.requireUserVerification); // 2-3. Parse allowCredentialDescriptorList // we do this slightly out of order, see below. // 4-5. Get keys that match this relying party ID List credentials = this.credentialSafe.getKeysForEntity(options.rpId); - Log.d(TAG, String.format("getAssertion - Got %d for rpID %s", credentials.size(), options.rpId)); // 2-3. Parse allowCredentialDescriptorList if (options.allowCredentialDescriptorList != null && options.allowCredentialDescriptorList.size() > 0) { @@ -394,7 +391,6 @@ public GetAssertionResult getAssertion(GetAssertionOptions options, CredentialSe Set allowedCredentialIds = new HashSet<>(); for (PublicKeyCredentialDescriptor descriptor: options.allowCredentialDescriptorList) { allowedCredentialIds.add(ByteBuffer.wrap(descriptor.id)); - Log.d(TAG, String.format("getAssertion - Filtering %s", Utils.bytesToHexString(descriptor.id))); } for (PublicKeyCredentialSource credential : credentials) { @@ -402,7 +398,6 @@ public GetAssertionResult getAssertion(GetAssertionOptions options, CredentialSe filteredCredentials.add(credential); } } - Log.d(TAG, String.format("getAssertion - Filtered credentials to %d", filteredCredentials.size())); credentials = filteredCredentials; } @@ -417,14 +412,11 @@ public GetAssertionResult getAssertion(GetAssertionOptions options, CredentialSe // 7. Allow the user to pick a specific credential, get verification PublicKeyCredentialSource selectedCredential; - if(!preFlight && selectedPreflightCredential != null && selectedPreflightCredential.rpId.equals(options.rpId)) { - Log.d(TAG, "getAssertion - Using selectedPreflightCredential: " + selectedPreflightCredential.userName); + if(!preFlight && selectedPreflightCredential != null) { selectedCredential = selectedPreflightCredential; } else if (credentials.size() == 1) { - Log.d(TAG, "getAssertion - credentials count is one!"); selectedCredential = credentials.get(0); } else { - Log.d(TAG, "getAssertion - Start credential selector"); selectedCredential = credentialSelector.selectFrom(credentials); if (selectedCredential == null) { throw new VirgilException("User did not select credential"); @@ -462,7 +454,6 @@ public GetAssertionResult getAssertion(GetAssertionOptions options, CredentialSe selectedCredential.rpName ) ); - selectedPreflightCredential = null; } if (!permission) { @@ -471,10 +462,7 @@ public GetAssertionResult getAssertion(GetAssertionOptions options, CredentialSe GetAssertionResult result = getInternalAssertion(options, selectedCredential, biometricSignature, uv, extensionOutput, preFlight); - - if(!preFlight) { - showToast(activity, "Authenticated for " + options.rpId, Toast.LENGTH_SHORT); - } + showToast(activity, "Authenticated for " + options.rpId, Toast.LENGTH_SHORT); return result; } @@ -540,10 +528,8 @@ public GetAssertionResult getInternalAssertion(GetAssertionOptions options, rpIdHash = WebAuthnCryptography.sha256(selectedCredential.u2fRpId); } - } else if (options.rpId != null) { - rpIdHash = WebAuthnCryptography.sha256(options.rpId); // 32 bytes } else { - throw new CtapException(CtapError.OTHER); + rpIdHash = WebAuthnCryptography.sha256(options.rpId); // 32 bytes } authenticatorData = constructAuthenticatorData(rpIdHash, null, @@ -800,11 +786,8 @@ public void onResult(boolean result, CryptoObject cryptoObject) { public Response authenticationRequest(RawMessages.AuthenticationRequest req, FragmentActivity activity) throws ApduException { - PublicKeyCredentialSource credentialSource = credentialSafe.getCredentialSourceById(req.keyHandle); - - if(credentialSource == null) { - throw new ApduException(StatusWord.WRONG_DATA); - } + PublicKeyCredentialSource credentialSource = credentialSafe.getCredentialSourceById( + req.keyHandle); boolean userVerification; if (KnownFacets.isDummyRequest(req.application, req.challenge)) { @@ -833,7 +816,7 @@ public Response authenticationRequest(RawMessages.AuthenticationRequest req, } } catch (VirgilException | CtapException e) { e.printStackTrace(); - throw new ApduException(StatusWord.WRONG_DATA); + throw new ApduException(StatusWord.MEMORY_FAILURE); } ByteBuffer buff = ByteBuffer.allocate(5 + assertion.signature.length); @@ -1101,8 +1084,15 @@ private AttestationObject constructAttestationObject(byte[] authenticatorData, b AttestationObject attestationObject; KeyPair keyPair = credentialSource.keyPair; try { + + // PackedSelfAttestion without X5C + // byte[] signatureBytes = this.cryptoProvider.performSignature(keyPair.getPrivate(), toSign, signature); + // attestationObject = new PackedSelfAttestation(authenticatorData, signatureBytes); + + // PackedSelfAttestion with X5C byte[] signatureBytes = this.cryptoProvider.performSignature(keyPair.getPrivate(), toSign, signature); - attestationObject = new PackedSelfAttestation(authenticatorData, signatureBytes); + attestationObject = new PackedSelfAttestation(authenticatorData, keyPair, signatureBytes); + } catch (VirgilException e) { Log.d(TAG, "Failed to create self-attestation statement - defaulting to NONE" + e); e.printStackTrace(); diff --git a/authorizer/src/main/java/net/tjado/webauthn/PasswdSafeCredentialBackend.java b/authorizer/src/main/java/net/tjado/webauthn/PasswdSafeCredentialBackend.java index 60297fdf2..4428bb0d8 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/PasswdSafeCredentialBackend.java +++ b/authorizer/src/main/java/net/tjado/webauthn/PasswdSafeCredentialBackend.java @@ -173,7 +173,7 @@ public PublicKeyCredentialSource generateCredential(@NonNull String rpEntityId, Log.w(TAG, "generateCredential"); if(!activity.isFileWritable()) { Log.w(TAG, "PasswdSafe File is not writeable... exit"); - throw new VirgilException("PasswdSafe File is not writeable"); + return null; } KeyPair keyPair = generateNewES256KeyPairLocal(); @@ -211,13 +211,13 @@ public PublicKeyCredentialSource generateCredential(@NonNull String rpEntityId, }); if (rc != null) { - activity.finishEditFidoRecord(rc); + activity.finishEditRecord(rc); } PublicKeyCredentialSource credentialSource; credentialSource = new PublicKeyCredentialSource(id[0], rpEntityId, rpDisplayName, userHandle, userName, userDisplayName, - generateHmacSecret, u2fRpId, keyPair, 0, null); + generateHmacSecret, u2fRpId, keyPair, null); if (generateHmacSecret && (symmetricKey == null)) { deleteCredential(credentialSource); @@ -227,20 +227,14 @@ public PublicKeyCredentialSource generateCredential(@NonNull String rpEntityId, return credentialSource; } - /** - * Check if a file record is currently in edit mode - */ - public boolean isEditMode() - { - return activity.isEditMode(); - } - /** * Use the file data with an optional record at the current location */ protected final RetT useRecordFile(final AbstractPasswdSafeLocationFragment.RecordFileUser user) { - return activity.useFileData(fileData -> user.useFile(null, fileData)); + return activity.useFileData(fileData -> { + return user.useFile(null, fileData); + }); } public String keyToString(Key key) { @@ -270,16 +264,7 @@ public SecretKey base64ToSecretKey(String base64Key) { } public PublicKeyCredentialSource recordToCredential(PasswdFileData fileData, PwsRecord rec) { - if(rec == null) { - return null; - } - - String fileKeyPair = fileData.getFidoKeyPair(rec); - if(fileKeyPair == null) { - return null; - } - - String[] keys = fileKeyPair.split(":"); + String[] keys = fileData.getFidoKeyPair(rec).split(":"); PublicKey publicKey = null; PrivateKey privateKey = null; @@ -292,7 +277,7 @@ public PublicKeyCredentialSource recordToCredential(PasswdFileData fileData, Pws KeyPair keyPair = new KeyPair(publicKey, privateKey); - return new PublicKeyCredentialSource( + PublicKeyCredentialSource credentialSource = new PublicKeyCredentialSource( fileData.getUUID(rec).getBytes(StandardCharsets.UTF_8), fileData.getFidoRpId(rec), fileData.getFidoRpName(rec), @@ -302,9 +287,10 @@ public PublicKeyCredentialSource recordToCredential(PasswdFileData fileData, Pws false, fileData.getFidoU2fRpId(rec), keyPair, - fileData.getFidoKeyUseCounter(rec), base64ToSecretKey(fileData.getFidoHmacSecret(rec)) ); + + return credentialSource; } public SecretKey generateSymmetricKey() { @@ -379,12 +365,7 @@ public List getKeysForEntity(@NonNull String rpEntity for (PwsRecord rec: fileData.getRecords()) { String rpId = fileData.getFidoRpId(rec); if (rpId != null && rpId.equals(rpEntityId)) { - PublicKeyCredentialSource cred = recordToCredential(fileData, rec); - if (cred != null) { - credentialSources.add(cred); - } else { - Log.d(TAG, "getKeysForEntity - credential is empty - skipping"); - } + credentialSources.add(recordToCredential(fileData, rec)); } } return null; @@ -526,7 +507,7 @@ public int incrementCredentialUseCounter(PublicKeyCredentialSource credential) { }); if (rc != null) { - activity.finishEditFidoRecord(rc); + activity.finishEditRecord(rc); } return currentCounter[0]; diff --git a/authorizer/src/main/java/net/tjado/webauthn/TransactionManager.java b/authorizer/src/main/java/net/tjado/webauthn/TransactionManager.java index 94b5a4553..62a551df4 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/TransactionManager.java +++ b/authorizer/src/main/java/net/tjado/webauthn/TransactionManager.java @@ -202,6 +202,7 @@ private boolean handleMessageIfComplete(Framing.SubmitReports submit) throws Cta CommandApdu u2fRequestApdu = new CommandApdu(payload); RawMessages.RequestU2F u2fRequest = RawMessages.parseU2Frequest(u2fRequestApdu); RawMessages.Response u2fResponse = handleU2F(activity, u2fRequest); + if (!u2fResponse.userVerification) { // No user presence check needed, continue right away ResponseApdu u2fResponseApdu = new ResponseApdu( @@ -300,21 +301,16 @@ public void handleReport(byte[] bytes, Framing.SubmitReports submit) { if (packet.channelId == 0) { throw new CtapHidException(CtapHidException.CtapHidError.InvalidCmd, packet.channelId); } - if (packet instanceof Framing.InitPacket) { Framing.InitPacket initPacket = (Framing.InitPacket)packet; - Log.d(TAG, "InitPacket Command: "+ initPacket.cmd); - switch (initPacket.cmd) { case Init: if (initPacket.totalLength != (short)Constants.INIT_CMD_NONCE_LENGTH) { - Log.d(TAG, "Wrong packet length: " + initPacket.totalLength); throw new CtapHidException(CtapHidException.CtapHidError.InvalidLen, packet.channelId); } if (message != null && (initPacket.channelId == message.channelId)) { // INIT command used to resync on the active channel. - Log.d(TAG, "INIT received on current channel; resetting transaction"); resetTransaction(); } int newChannelId; @@ -324,10 +320,8 @@ public void handleReport(byte[] bytes, Framing.SubmitReports submit) { if (freeChannelId == Constants.BROADCAST_CHANNEL_ID) { freeChannelId = 1; } - Log.d(TAG, "Assigning new channel ID: " + newChannelId); } else { newChannelId = packet.channelId; - Log.d(TAG, "Using previous channel ID: " + newChannelId); } submit.submit(new Framing.InitResponse( packet.channelId, @@ -337,9 +331,8 @@ public void handleReport(byte[] bytes, Framing.SubmitReports submit) { case Cancel: if (message == null) return; if (message.channelId == packet.channelId) { - Log.d(TAG, "Cancelling current transaction"); - if (activeCborJob != null) activeCborJob.cancel(true); + Log.i(TAG, "Cancelling current transaction"); authenticator.cancelBiometricPrompt(); activeCborJob = null; } @@ -371,16 +364,13 @@ public void handleReport(byte[] bytes, Framing.SubmitReports submit) { } else if (packet instanceof Framing.ContPacket) { if (packet.channelId == Constants.BROADCAST_CHANNEL_ID) { // Only INIT messages are allowed on the broadcast channel. - Log.d(TAG, "Only INIT messages are allowed on the broadcast channel."); throw new CtapHidException(CtapHidException.CtapHidError.InvalidCid, packet.channelId); } if (message != null && (!message.append((Framing.ContPacket)packet))) { - Log.d(TAG, "Spurious continuation packet dropped"); // Spurious continuation packets are dropped without timeout renewal. return; } } - haltTimeout(); if (!handleMessageIfComplete(submit)) { startTimeout(submit); diff --git a/authorizer/src/main/java/net/tjado/webauthn/fido/hid/Constants.java b/authorizer/src/main/java/net/tjado/webauthn/fido/hid/Constants.java index b47b7fe4b..2c8dd40c1 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/fido/hid/Constants.java +++ b/authorizer/src/main/java/net/tjado/webauthn/fido/hid/Constants.java @@ -11,7 +11,7 @@ import kotlin.UInt; public class Constants { - public static final int HID_REPORT_SIZE = 64; + public static final int HID_REPORT_SIZE = 62; static final int INIT_PACKET_PAYLOAD_SIZE = HID_REPORT_SIZE - 7; static final int CONT_PACKET_PAYLOAD_SIZE = HID_REPORT_SIZE - 5; diff --git a/authorizer/src/main/java/net/tjado/webauthn/fido/hid/Framing.java b/authorizer/src/main/java/net/tjado/webauthn/fido/hid/Framing.java index 0e8849e0b..0ce339876 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/fido/hid/Framing.java +++ b/authorizer/src/main/java/net/tjado/webauthn/fido/hid/Framing.java @@ -1,7 +1,5 @@ package net.tjado.webauthn.fido.hid; -import android.util.Log; - import org.jetbrains.annotations.NotNull; import java.io.ByteArrayOutputStream; @@ -11,7 +9,6 @@ import java.util.List; import java.util.Locale; -import net.tjado.passwdsafe.lib.Utils; import net.tjado.webauthn.exceptions.CtapHidException; import net.tjado.webauthn.exceptions.CtapHidException.CtapHidError; import net.tjado.webauthn.fido.hid.Constants.CtapHidCommand; @@ -31,13 +28,11 @@ public Packet(int channelId, byte[] payload) { abstract byte[] toRawReport(); public static Packet parse(byte[] bytes) throws CtapHidException { - Log.d("Framing", "Parsing Packet: " + Utils.bytesToHexString(bytes)); - int reportOffset; if (bytes.length == (Constants.HID_REPORT_SIZE) + 1) { reportOffset = 1; // Linux (hidraw) includes the report ID - } else if (bytes.length == Constants.HID_REPORT_SIZE || bytes.length == Constants.HID_REPORT_SIZE - 2) { - reportOffset = 0; // Windows, iOS and macOS does not include the report ID + } else if (bytes.length == Constants.HID_REPORT_SIZE) { + reportOffset = 0; // Windows (hidsdi.h) does not include the report ID } else { throw new CtapHidException(CtapHidError.InvalidLen, bytes.length); } @@ -45,7 +40,6 @@ public static Packet parse(byte[] bytes) throws CtapHidException { int channelId = bytes2int(bytes, reportOffset); if ((bytes[reportOffset + 4] & Constants.MESSAGE_TYPE_MASK) == Constants.MESSAGE_TYPE_INIT) { // Initialization packet - Log.i("Framing", "Parsing Init packet"); CtapHidCommand cmd; try { cmd = CtapHidCommand.fromByte((byte)(bytes[reportOffset + 4] & (byte)Constants.COMMAND_MASK)); @@ -60,7 +54,6 @@ public static Packet parse(byte[] bytes) throws CtapHidException { return new InitPacket(channelId, cmd, totalLength, data); } else { // Continuation packet - Log.i("Framing", "Parsing Continuation packet"); byte seq = bytes[reportOffset + 4]; byte[] data = Arrays.copyOfRange(bytes, reportOffset + 4 + 1, bytes.length); return new ContPacket(channelId, seq, data); @@ -163,7 +156,6 @@ public byte[] getPayloadIfComplete() { public boolean append(ContPacket packet) throws CtapHidException { if (packet.channelId != channelId) { // Spurious continuation packets are dropped without error. - Log.d("Framing", "Spurious continuation packet dropped."); return false; } if (isComplete() || packet.seq != seq) { diff --git a/authorizer/src/main/java/net/tjado/webauthn/models/GetAssertionOptions.java b/authorizer/src/main/java/net/tjado/webauthn/models/GetAssertionOptions.java index 9cc206041..d74cabb6f 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/models/GetAssertionOptions.java +++ b/authorizer/src/main/java/net/tjado/webauthn/models/GetAssertionOptions.java @@ -81,11 +81,13 @@ public GetAssertionOptions fromCBor(Map inputMap) { uv = (options.get(new UnicodeString("uv"))).equals(SimpleValue.TRUE); } catch (Exception ignore) {} - if (up == null) { - up = true; // UP is true by default - } - if (uv == null) { - uv = false; // UV is false by default + if (up == null && uv == null) { + up = true; + uv = false; + } else if (up != null) { + uv = !up; + } else { + up = !uv; } requireUserPresence = up; @@ -128,6 +130,11 @@ public void areWellFormed() throws CtapException { "Client data hash of length: " + clientDataHash.length); } + if (requireUserPresence == requireUserVerification) { // only one may be set + throw new CtapException(CtapError.UNSUPPORTED_OPTION, "Both uv and up are " + + requireUserVerification); + } + ClientPINOptions.pinOptionsWellFormed(pinProtocol, pinAuth); } diff --git a/authorizer/src/main/java/net/tjado/webauthn/models/MakeCredentialOptions.java b/authorizer/src/main/java/net/tjado/webauthn/models/MakeCredentialOptions.java index 98da57185..9244d24df 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/models/MakeCredentialOptions.java +++ b/authorizer/src/main/java/net/tjado/webauthn/models/MakeCredentialOptions.java @@ -168,11 +168,13 @@ public MakeCredentialOptions fromCBor(Map inputMap) { uv = (options.get(new UnicodeString("uv"))).equals(SimpleValue.TRUE); } catch (Exception ignore) {} - if (up == null) { - up = true; // UP is true by default - } - if (uv == null) { - uv = false; // UV is false by default + if (up == null && uv == null) { + up = true; + uv = false; + } else if (up != null) { + uv = !up; + } else { + up = !uv; } requireUserPresence = up; @@ -223,6 +225,11 @@ public void areWellFormed() throws CtapException { "User id with invalind length: " + userEntity.id.length); } + if (requireUserPresence == requireUserVerification) { // only one may be set + throw new CtapException(CtapError.UNSUPPORTED_OPTION, "Both uv and up are " + + requireUserVerification); + } + ClientPINOptions.pinOptionsWellFormed(pinProtocol, pinAuth); } diff --git a/authorizer/src/main/java/net/tjado/webauthn/models/PackedSelfAttestation.java b/authorizer/src/main/java/net/tjado/webauthn/models/PackedSelfAttestation.java index f60404fee..cfbce7f3a 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/models/PackedSelfAttestation.java +++ b/authorizer/src/main/java/net/tjado/webauthn/models/PackedSelfAttestation.java @@ -7,22 +7,100 @@ import net.tjado.webauthn.fido.ctap2.CtapSuccessOutputStream; import net.tjado.webauthn.fido.ctap2.Messages; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import java.math.BigInteger; +import java.security.*; +import java.security.cert.X509Certificate; +import java.util.Calendar; +import java.util.Date; + public class PackedSelfAttestation extends AttestationObject { /** * Construct a new self-attestation attObj in packed format. * * @param authData The authenticator data signed. + * @param keyPair Generated credential keypair * @param signature The signature over the concatenation of authenticatorData and * clientDataHash. */ - public PackedSelfAttestation(byte[] authData, byte[] signature) { + public PackedSelfAttestation(byte[] authData, KeyPair keyPair, byte[] signature) { this.authData = authData; this.fmt = FormatType.PACKED; this.signature = signature; - this.certificate = null; + + try + { + Security.addProvider(new BouncyCastleProvider()); + + // Subject + X500Name subject = new X500Name("C=DE,O=Authorizer Passkey,OU=Authenticator Attestation,CN=Self-Signed Virtual FIDO2"); + + // Validity + Calendar calendar = Calendar.getInstance(); + Date notBefore = calendar.getTime(); + calendar.add(Calendar.YEAR, 10); + Date notAfter = calendar.getTime(); + + // Certificate builder + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + subject, + BigInteger.valueOf(System.currentTimeMillis()), + notBefore, + notAfter, + subject, + keyPair.getPublic() + ); + + // Add Basic Constraints extension + certBuilder.addExtension( + new ASN1ObjectIdentifier("2.5.29.19"), // OID for Basic Constraints + true, // critical + new BasicConstraints(false) // CA=false + ); + + // Signer + ContentSigner signer = new JcaContentSignerBuilder("SHA256withECDSA") + .setProvider("AndroidOpenSSL").build(keyPair.getPrivate()); + + // Build cert + X509Certificate cert = new JcaX509CertificateConverter() + .setProvider("AndroidOpenSSL") + .getCertificate(certBuilder.build(signer)); + + this.certificate = cert.getEncoded(); + } + catch (Exception e) + { + System.out.println(e.toString()); + } + } +// /** +// * Construct a new self-attestation attObj in packed format. +// * +// * @param authData The authenticator data signed. +// * @param signature The signature over the concatenation of authenticatorData and +// * clientDataHash. +// */ +// public PackedSelfAttestation(byte[] authData, byte[] signature) { +// this.authData = authData; +// this.fmt = FormatType.PACKED; +// this.signature = signature; +// this.certificate = null; +// } + + /** * Encode this self-attestation attObj as the CBOR required by the WebAuthn spec * https://www.w3.org/TR/webauthn/#sctn-attestation @@ -42,6 +120,9 @@ public byte[] asCBOR() throws VirgilException { .putMap(Messages.MAKE_CREDENTIAL_RESPONSE_ATT_STMT) .put("alg", AlgType.ES256) .put("sig", this.signature) + .putArray("x5c") + .add(this.certificate) + .end() .end() .end() .build() diff --git a/authorizer/src/main/java/net/tjado/webauthn/models/PublicKeyCredentialSource.java b/authorizer/src/main/java/net/tjado/webauthn/models/PublicKeyCredentialSource.java index 2cd3cb6a0..68c691a0e 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/models/PublicKeyCredentialSource.java +++ b/authorizer/src/main/java/net/tjado/webauthn/models/PublicKeyCredentialSource.java @@ -50,7 +50,7 @@ public class PublicKeyCredentialSource { */ public PublicKeyCredentialSource(byte[] id, @NonNull String rpId, String rpName, byte[] userHandle, String userName, String userDisplayName, boolean generateHmacSecret, - String u2fRpId, KeyPair keyPair, Integer keyUseCounter, SecretKey hmacSecret) { + String u2fRpId, KeyPair keyPair, SecretKey hmacSecret) { ensureRandomInitialized(); this.id = id; @@ -63,12 +63,7 @@ public PublicKeyCredentialSource(byte[] id, @NonNull String rpId, String rpName, this.u2fRpId = u2fRpId; this.keyPair = keyPair; - - if (keyUseCounter != null) { - this.keyUseCounter = keyUseCounter; - } else { - this.keyUseCounter = 0; - } + this.keyUseCounter = 1; this.hmacSecret = hmacSecret; diff --git a/authorizer/src/main/java/net/tjado/webauthn/models/U2fSelfAttestation.java b/authorizer/src/main/java/net/tjado/webauthn/models/U2fSelfAttestation.java index 70f3f8c87..c09ba44e9 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/models/U2fSelfAttestation.java +++ b/authorizer/src/main/java/net/tjado/webauthn/models/U2fSelfAttestation.java @@ -53,12 +53,10 @@ public U2fSelfAttestation(byte[] authData, WebAuthnCryptography cryptoProvider, throws VirgilException { this.authData = authData; this.fmt = FormatType.U2F_LEGACY; - + this.certificate = null; KeyPair keyPair = credentialSource.keyPair; - this.certificate = keyPair.getPublic().getEncoded(); - // Sign the content try { this.signature = cryptoProvider.performSignature(keyPair.getPrivate(), this.authData, null); diff --git a/authorizer/src/main/java/net/tjado/webauthn/util/ClientPinLocker.java b/authorizer/src/main/java/net/tjado/webauthn/util/ClientPinLocker.java index 4b4019e00..e35d170ba 100644 --- a/authorizer/src/main/java/net/tjado/webauthn/util/ClientPinLocker.java +++ b/authorizer/src/main/java/net/tjado/webauthn/util/ClientPinLocker.java @@ -21,7 +21,6 @@ import java.security.SecureRandom; import java.security.cert.CertificateException; -import net.tjado.passwdsafe.lib.Utils; import net.tjado.webauthn.exceptions.VirgilException; @RequiresApi(api = Build.VERSION_CODES.O) @@ -61,7 +60,7 @@ public ClientPinLocker(Context ctx, throw new VirgilException("Failed to hash data", e); } md.update(clientData); - this.clientId = Utils.bytesToHexString(md.digest()); + this.clientId = bytesToHexString(md.digest()); this.cpkAlias = "cpk" + this.clientId; this.lockerId = CLIENT_PIN_TYPE + this.clientId; @@ -81,7 +80,7 @@ public ClientPinLocker setRetries(long retries) { public ClientPinLocker setToken(@NonNull byte[] token) { if (token.length % 16 != 0) return this; - String tokenString = Utils.bytesToHexString(token); + String tokenString = bytesToHexString(token); locker.edit() .putString(CLIENT_PIN_FIELD_PIN_TOKEN, tokenString) .apply(); @@ -89,7 +88,7 @@ public ClientPinLocker setToken(@NonNull byte[] token) { } public boolean lockPin(@NonNull byte[] pin) { - String pinString = Utils.bytesToHexString(pin); + String pinString = bytesToHexString(pin); try { refreshToken(); } catch (VirgilException e) { @@ -101,7 +100,7 @@ public boolean lockPin(@NonNull byte[] pin) { } public boolean isPinMatch(@NonNull byte[] pinTry) { - String pinTryString = Utils.bytesToHexString(pinTry); + String pinTryString = bytesToHexString(pinTry); String pinReference = locker.getString(CLIENT_PIN_FIELD_PIN_SHA256, CLIENT_PIN_FIELD_PIN_SHA256_VALUE); @@ -208,6 +207,16 @@ private String getLockerUri() { return appInfo.dataDir + "/shared_prefs/" + lockerId + ".xml"; } + private String bytesToHexString(@NonNull byte[] bytes) { + StringBuilder hexString = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(0xFF & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + return hexString.toString(); + } + private @Nullable byte[] hexStringToBytes(String str) { int len = str.length(); if (len == 0) return null; diff --git a/authorizer/src/main/res/layout/activity_otp_add.xml b/authorizer/src/main/res/layout/activity_otp_add.xml index ccd691cbd..d4d64d731 100644 --- a/authorizer/src/main/res/layout/activity_otp_add.xml +++ b/authorizer/src/main/res/layout/activity_otp_add.xml @@ -19,8 +19,7 @@ - limitations under the License. --> @@ -129,16 +128,14 @@ android:layout_height="match_parent" android:layout_weight="1" android:checked="true" - android:text="6" - tools:ignore="HardcodedText" /> + android:text="6" /> + android:text="8" /> @@ -180,8 +177,7 @@ android:layout_height="match_parent" android:layout_weight="1" android:inputType="number" - android:text="30" - tools:ignore="HardcodedText" /> + android:text="30" /> + android:text="0" /> diff --git a/authorizer/src/main/res/layout/fragment_passwdsafe_open_file.xml b/authorizer/src/main/res/layout/fragment_passwdsafe_open_file.xml index 58ef073c5..1c58c3007 100644 --- a/authorizer/src/main/res/layout/fragment_passwdsafe_open_file.xml +++ b/authorizer/src/main/res/layout/fragment_passwdsafe_open_file.xml @@ -11,10 +11,10 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" - android:paddingTop="@dimen/activity_vertical_margin" android:paddingRight="@dimen/activity_horizontal_margin" - android:paddingBottom="@dimen/activity_vertical_margin" + android:paddingTop="@dimen/activity_vertical_margin" tools:context="net.tjado.passwdsafe.PasswdSafeOpenFileFragment"> - - + app:layout_constraintTop_toBottomOf="@+id/passwd_input"/> + app:layout_constraintTop_toBottomOf="@+id/passwd_input"/> - - - - - - @@ -155,7 +107,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="8dp" - app:layout_constraintBottom_toTopOf="@id/saved_password" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/ro_save_options"/> @@ -169,22 +120,35 @@ android:gravity="center_vertical" app:drawableLeftCompat="@drawable/ic_fingerprint" app:drawableStartCompat="@drawable/ic_fingerprint" - app:layout_constraintBottom_toTopOf="@+id/yubi_progress_text" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/read_only_msg"/> + app:layout_constraintTop_toBottomOf="@+id/open"/> + + + app:layout_constraintBottom_toTopOf="@+id/open" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/yubi_progress_text" + app:layout_constraintTop_toBottomOf="@+id/saved_password"/> + +