mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-25 23:14:18 +03:00
Merge PR #180.
This commit is contained in:
commit
6080d2985e
@ -30,7 +30,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
compileSdkVersion 32
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
@ -46,7 +46,7 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.yubico.yubioath"
|
||||
applicationId "com.yubico.yubioath.preview"
|
||||
minSdkVersion project.minSdkVersion
|
||||
targetSdkVersion project.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
@ -83,10 +83,11 @@ dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
|
||||
|
||||
// Lifecycle
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.0'
|
||||
|
||||
// testing dependencies
|
||||
testImplementation "junit:junit:$project.junitVersion"
|
||||
|
@ -1,3 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.yubico.authenticator">
|
||||
|
||||
@ -15,15 +16,18 @@
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_label">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="false"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
@ -41,10 +45,27 @@
|
||||
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||
android:resource="@xml/device_filter" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".NdefActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/NdefActivityTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data
|
||||
android:host="my.yubico.com"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
</manifest>
|
@ -0,0 +1,7 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
class Constants {
|
||||
companion object {
|
||||
const val EXTRA_OPENED_THROUGH_NFC = "openedThroughNfcTap"
|
||||
}
|
||||
}
|
@ -4,10 +4,12 @@ import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.nfc.NfcAdapter
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.yubico.authenticator.Constants.Companion.EXTRA_OPENED_THROUGH_NFC
|
||||
import com.yubico.authenticator.logging.FlutterLog
|
||||
import com.yubico.authenticator.logging.Log
|
||||
import com.yubico.authenticator.oath.OathManager
|
||||
@ -45,6 +47,16 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
yubikit = YubiKitManager(this)
|
||||
|
||||
setupYubiKeyDiscovery()
|
||||
setupYubiKitLogger()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
}
|
||||
|
||||
private fun setupYubiKeyDiscovery() {
|
||||
viewModel.handleYubiKey.observe(this) {
|
||||
if (it) {
|
||||
Log.d(TAG, "Starting usb discovery")
|
||||
@ -59,8 +71,6 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
Log.d(TAG, "Stopped usb discovery")
|
||||
}
|
||||
}
|
||||
|
||||
setupYubiKitLogger()
|
||||
}
|
||||
|
||||
fun startNfcDiscovery(): Boolean =
|
||||
@ -118,6 +128,17 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
try {
|
||||
if (intent.getBooleanExtra(EXTRA_OPENED_THROUGH_NFC, false)) {
|
||||
// make nfc available to yubikit
|
||||
NfcAdapter.getDefaultAdapter(this).disableReaderMode(this)
|
||||
setupYubiKeyDiscovery()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failure when resuming YubiKey discovery", e.stackTraceToString())
|
||||
}
|
||||
|
||||
startNfcDiscovery()
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,129 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.nfc.NdefMessage
|
||||
import android.nfc.NfcAdapter
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import com.yubico.authenticator.Constants.Companion.EXTRA_OPENED_THROUGH_NFC
|
||||
import com.yubico.authenticator.logging.Log
|
||||
import com.yubico.authenticator.yubiclip.scancode.KeyboardLayout
|
||||
import com.yubico.yubikit.core.util.NdefUtils
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
typealias ResourceId = Int
|
||||
|
||||
class NdefActivity : Activity() {
|
||||
|
||||
private var openAppOnNfcTap: Boolean = false
|
||||
private var copyOtpOnNfcTap: Boolean = false
|
||||
private lateinit var clipKbdLayout: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val prefs: SharedPreferences = getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)
|
||||
openAppOnNfcTap = prefs.getBoolean(PREF_NFC_OPEN_APP, false)
|
||||
copyOtpOnNfcTap = prefs.getBoolean(PREF_NFC_COPY_OTP, false)
|
||||
clipKbdLayout = prefs.getString(PREF_CLIP_KBD_LAYOUT, DEFAULT_CLIP_KBD_LAYOUT)!!
|
||||
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
overridePendingTransition(0, 0)
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent) {
|
||||
intent.data?.let {
|
||||
if (copyOtpOnNfcTap) {
|
||||
try {
|
||||
val otpSlotContent = parseOtpFromIntent()
|
||||
setPrimaryClip(otpSlotContent.content)
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
showToast(
|
||||
when (otpSlotContent.type) {
|
||||
OtpType.Otp -> R.string.otp_success_set_otp_to_clipboard
|
||||
OtpType.Password -> R.string.otp_success_set_password_to_clipboard
|
||||
}, Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
|
||||
} catch (illegalArgumentException: IllegalArgumentException) {
|
||||
Log.e(
|
||||
TAG,
|
||||
illegalArgumentException.message ?: "Failure when handling YubiKey OTP",
|
||||
illegalArgumentException.stackTraceToString()
|
||||
)
|
||||
showToast(R.string.otp_parse_failure, Toast.LENGTH_LONG)
|
||||
} catch (_: UnsupportedOperationException) {
|
||||
showToast(R.string.otp_set_clip_failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
|
||||
if (openAppOnNfcTap) {
|
||||
val mainAppIntent = Intent(this, MainActivity::class.java).apply {
|
||||
putExtra(EXTRA_OPENED_THROUGH_NFC, true)
|
||||
}
|
||||
startActivity(mainAppIntent)
|
||||
}
|
||||
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showToast(value: ResourceId, length: Int) {
|
||||
Toast.makeText(this, value, length).show()
|
||||
}
|
||||
|
||||
private fun parseOtpFromIntent(): OtpSlotValue {
|
||||
val parcelable = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
|
||||
if (parcelable != null && parcelable.isNotEmpty()) {
|
||||
val ndefPayloadBytes =
|
||||
NdefUtils.getNdefPayloadBytes((parcelable[0] as NdefMessage).toByteArray())
|
||||
|
||||
return if (ndefPayloadBytes.all { it in 32..126 }) {
|
||||
OtpSlotValue(OtpType.Otp, String(ndefPayloadBytes, StandardCharsets.US_ASCII))
|
||||
} else {
|
||||
val kbd: KeyboardLayout = KeyboardLayout.forName(clipKbdLayout)
|
||||
OtpSlotValue(OtpType.Password, kbd.fromScanCodes(ndefPayloadBytes))
|
||||
}
|
||||
}
|
||||
throw IllegalArgumentException("Failed to parse OTP from the intent")
|
||||
}
|
||||
|
||||
private fun setPrimaryClip(otp: String) {
|
||||
try {
|
||||
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(otp, otp))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to copy otp string to clipboard", e.stackTraceToString())
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "YubicoAuthenticatorOTPActivity"
|
||||
const val PREFS_FILE = "FlutterSharedPreferences"
|
||||
const val PREF_NFC_OPEN_APP = "flutter.prefNfcOpenApp"
|
||||
const val PREF_NFC_COPY_OTP = "flutter.prefNfcCopyOtp"
|
||||
|
||||
const val PREF_CLIP_KBD_LAYOUT = "flutter.prefClipKbdLayout"
|
||||
const val DEFAULT_CLIP_KBD_LAYOUT = "US"
|
||||
}
|
||||
|
||||
enum class OtpType {
|
||||
Otp, Password
|
||||
}
|
||||
|
||||
data class OtpSlotValue(val type: OtpType, val content: String)
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package com.yubico.authenticator.yubiclip.scancode;
|
||||
|
||||
/**
|
||||
* Created by mjenny on 7/12/15.
|
||||
*/
|
||||
public class DECHKeyboardLayout extends KeyboardLayout {
|
||||
private static final String[] usb2key1 = new String[]{
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g", /* 0xa */
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"q", /* 0x14 */
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"z",
|
||||
"y",
|
||||
"1", /* 0x1e */
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"0",
|
||||
"\n", /* 0x28 */
|
||||
"",
|
||||
"",
|
||||
"\t",
|
||||
" ",
|
||||
"'",
|
||||
"^",
|
||||
"ü",
|
||||
"¨",
|
||||
"",
|
||||
"$",
|
||||
"ö",
|
||||
"ä",
|
||||
"§",
|
||||
",",
|
||||
".",
|
||||
"-", /* 0x38 */
|
||||
};
|
||||
private static final String[] usb2key2 = new String[]{
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G", /* 0x8a */
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"Q", /* 0x94 */
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Z",
|
||||
"Y",
|
||||
"+",
|
||||
"\"",
|
||||
"*",
|
||||
"ç",
|
||||
"%",
|
||||
"&",
|
||||
"/",
|
||||
"(",
|
||||
")",
|
||||
"=",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"?",
|
||||
"`",
|
||||
"è",
|
||||
"!",
|
||||
"",
|
||||
"£",
|
||||
"Ö",
|
||||
"Ä",
|
||||
"°",
|
||||
";",
|
||||
":",
|
||||
"_",
|
||||
};
|
||||
|
||||
@Override
|
||||
protected String fromScanCode(int code) {
|
||||
if (code < SHIFT) {
|
||||
if (code < usb2key1.length) {
|
||||
return usb2key1[code];
|
||||
}
|
||||
} else {
|
||||
code = code ^ SHIFT;
|
||||
if (code < usb2key2.length) {
|
||||
return usb2key2[code];
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package com.yubico.authenticator.yubiclip.scancode;
|
||||
|
||||
/**
|
||||
* Created by kemiren on 6/18/14.
|
||||
*/
|
||||
public class DEKeyboardLayout extends KeyboardLayout {
|
||||
private static final String[] usb2key1 = new String[]{
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g", /* 0xa */
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"q", /* 0x14 */
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"z",
|
||||
"y",
|
||||
"1", /* 0x1e */
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"0",
|
||||
"\n", /* 0x28 */
|
||||
"",
|
||||
"",
|
||||
"\t",
|
||||
" ",
|
||||
"ß",
|
||||
"´",
|
||||
"ü",
|
||||
"+",
|
||||
"",
|
||||
"#",
|
||||
"ö",
|
||||
"'",
|
||||
"^",
|
||||
",",
|
||||
".",
|
||||
"-", /* 0x38 */
|
||||
};
|
||||
private static final String[] usb2key2 = new String[]{
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G", /* 0x8a */
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"Q", /* 0x94 */
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Z",
|
||||
"Y",
|
||||
"!",
|
||||
"\"",
|
||||
"§",
|
||||
"$",
|
||||
"%",
|
||||
"&",
|
||||
"/",
|
||||
"(",
|
||||
")",
|
||||
"=",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"?",
|
||||
"`",
|
||||
"Ü",
|
||||
"*",
|
||||
"",
|
||||
">",
|
||||
"Ö",
|
||||
"Ä",
|
||||
"'",
|
||||
";",
|
||||
":",
|
||||
"_",
|
||||
};
|
||||
|
||||
@Override
|
||||
protected String fromScanCode(int code) {
|
||||
if (code < SHIFT) {
|
||||
if (code < usb2key1.length) {
|
||||
return usb2key1[code];
|
||||
}
|
||||
} else {
|
||||
code = code ^ SHIFT;
|
||||
if (code < usb2key2.length) {
|
||||
return usb2key2[code];
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.yubico.authenticator.yubiclip.scancode;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Created by dain on 2/17/14.
|
||||
*/
|
||||
public abstract class KeyboardLayout {
|
||||
private static final Map<String, KeyboardLayout> layouts = new HashMap<String, KeyboardLayout>();
|
||||
|
||||
static {
|
||||
layouts.put("US", new USKeyboardLayout());
|
||||
layouts.put("DE", new DEKeyboardLayout());
|
||||
layouts.put("DE-CH", new DECHKeyboardLayout());
|
||||
}
|
||||
|
||||
public static KeyboardLayout forName(String name) {
|
||||
return layouts.get(name.toUpperCase());
|
||||
}
|
||||
|
||||
public static Set<String> availableLayouts() {
|
||||
return new TreeSet<>(layouts.keySet());
|
||||
}
|
||||
|
||||
protected static final int SHIFT = 0x80;
|
||||
|
||||
protected abstract String fromScanCode(int code);
|
||||
|
||||
public final String fromScanCodes(byte[] bytes) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
buf.append(fromScanCode(b & 0xff));
|
||||
}
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
package com.yubico.authenticator.yubiclip.scancode;
|
||||
|
||||
/**
|
||||
* Created by dain on 2/17/14.
|
||||
*/
|
||||
public class USKeyboardLayout extends KeyboardLayout {
|
||||
private static final String[] usb2key1 = new String[]{
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g", /* 0xa */
|
||||
"h",
|
||||
"i",
|
||||
"j",
|
||||
"k",
|
||||
"l",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"p",
|
||||
"q", /* 0x14 */
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"x",
|
||||
"y",
|
||||
"z",
|
||||
"1", /* 0x1e */
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"0",
|
||||
"\n", /* 0x28 */
|
||||
"",
|
||||
"",
|
||||
"\t",
|
||||
" ",
|
||||
"-",
|
||||
"=",
|
||||
"[",
|
||||
"]",
|
||||
"",
|
||||
"\\",
|
||||
";",
|
||||
"'",
|
||||
"`",
|
||||
",",
|
||||
".",
|
||||
"/", /* 0x38 */
|
||||
};
|
||||
private static final String[] usb2key2 = new String[]{
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G", /* 0x8a */
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
"K",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"Q", /* 0x94 */
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"!",
|
||||
"@",
|
||||
"#",
|
||||
"$",
|
||||
"%",
|
||||
"^",
|
||||
"&",
|
||||
"*",
|
||||
"(",
|
||||
")",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"_",
|
||||
"+",
|
||||
"{",
|
||||
"}",
|
||||
"",
|
||||
"|",
|
||||
":",
|
||||
"\"",
|
||||
"~",
|
||||
"<",
|
||||
">",
|
||||
"?",
|
||||
};
|
||||
|
||||
@Override
|
||||
protected String fromScanCode(int code) {
|
||||
if (code < SHIFT) {
|
||||
if (code < usb2key1.length) {
|
||||
return usb2key1[code];
|
||||
}
|
||||
} else {
|
||||
code = code ^ SHIFT;
|
||||
if (code < usb2key2.length) {
|
||||
return usb2key2[code];
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_label">Yubico Authenticator</string>
|
||||
<string name="app_label">Yubico Authenticator Preview</string>
|
||||
<string name="otp_success_set_otp_to_clipboard">Successfully copied OTP code from YubiKey to clipboard.</string>
|
||||
<string name="otp_success_set_password_to_clipboard">Successfully copied password from YubiKey to clipboard.</string>
|
||||
<string name="otp_parse_failure">Failed to parse OTP code from YubiKey.</string>
|
||||
<string name="otp_set_clip_failure">Failed to access clipboard when trying to copy OTP code from YubiKey.</string>
|
||||
</resources>
|
@ -15,4 +15,15 @@
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
|
||||
<style name="NdefActivityTheme" parent="NormalTheme">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowAnimationStyle">@null</item>
|
||||
<item name="android:windowDisablePreview">true</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:windowIsFloating">true</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
@ -1,5 +1,5 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.6.21'
|
||||
ext.kotlin_version = '1.7.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
@ -7,12 +7,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'app/state.dart';
|
||||
import 'version.dart';
|
||||
import 'app/logging.dart';
|
||||
import 'app/message.dart';
|
||||
import 'app/state.dart';
|
||||
import 'core/state.dart';
|
||||
import 'desktop/state.dart';
|
||||
import 'version.dart';
|
||||
import 'widgets/responsive_dialog.dart';
|
||||
|
||||
final _log = Logger('about');
|
||||
@ -31,7 +31,9 @@ class AboutPage extends ConsumerWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0),
|
||||
child: Text(
|
||||
'Yubico Authenticator',
|
||||
Platform.isAndroid
|
||||
? 'Yubico Authenticator Preview'
|
||||
: 'Yubico Authenticator',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
|
@ -13,8 +13,8 @@ import 'package:yubico_authenticator/app/state.dart';
|
||||
import 'package:yubico_authenticator/core/models.dart';
|
||||
import 'package:yubico_authenticator/oath/state.dart';
|
||||
|
||||
import '../../oath/models.dart';
|
||||
import '../../cancellation_exception.dart';
|
||||
import '../../oath/models.dart';
|
||||
import 'command_providers.dart';
|
||||
|
||||
final _log = Logger('android.oath.state');
|
||||
@ -186,7 +186,9 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
|
||||
|
||||
var result = jsonDecode(resultString);
|
||||
var addedCredential = OathCredential.fromJson(result['credential']);
|
||||
var addedCredCode = OathCode.fromJson(result['code']);
|
||||
|
||||
var addedCredCode =
|
||||
result['code'] != null ? OathCode.fromJson(result['code']) : null;
|
||||
|
||||
if (mounted) {
|
||||
final newState = state!.toList();
|
||||
|
142
lib/android/views/android_settings_page.dart
Executable file
142
lib/android/views/android_settings_page.dart
Executable file
@ -0,0 +1,142 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:yubico_authenticator/core/state.dart';
|
||||
|
||||
import '../../app/state.dart';
|
||||
import '../../widgets/list_title.dart';
|
||||
import '../../widgets/responsive_dialog.dart';
|
||||
|
||||
class AndroidSettingsPage extends ConsumerWidget {
|
||||
const AndroidSettingsPage({super.key});
|
||||
|
||||
static const String prefNfcOpenApp = 'prefNfcOpenApp';
|
||||
static const String prefNfcCopyOtp = 'prefNfcCopyOtp';
|
||||
static const String prefClipKbdLayout = 'prefClipKbdLayout';
|
||||
|
||||
static const String defaultClipKbdLayout = 'US';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final nfcOpenApp = ref.watch(prefProvider).getBool(prefNfcOpenApp) ?? false;
|
||||
final nfcCopyOtp = ref.watch(prefProvider).getBool(prefNfcCopyOtp) ?? false;
|
||||
final clipKbdLayout =
|
||||
ref.watch(prefProvider).getString(prefClipKbdLayout) ??
|
||||
defaultClipKbdLayout;
|
||||
final themeMode = ref.watch(themeModeProvider);
|
||||
return ResponsiveDialog(
|
||||
title: const Text('Settings'),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ListTitle('General'),
|
||||
SwitchListTile(
|
||||
title: const Text('Open authenticator on NFC tap'),
|
||||
value: nfcOpenApp,
|
||||
onChanged: (value) {
|
||||
ref.read(prefProvider).setBool(prefNfcOpenApp, value);
|
||||
ref.refresh(prefProvider);
|
||||
}),
|
||||
const ListTitle('Yubiclip'),
|
||||
SwitchListTile(
|
||||
title: const Text('Copy OTP to clipboard'),
|
||||
value: nfcCopyOtp,
|
||||
onChanged: (value) {
|
||||
ref.read(prefProvider).setBool(prefNfcCopyOtp, value);
|
||||
ref.refresh(prefProvider);
|
||||
}),
|
||||
ListTile(
|
||||
title: const Text('Static password keyboard layout'),
|
||||
subtitle: Text('Current: $clipKbdLayout'),
|
||||
onTap: () async {
|
||||
var newValue = await _selectKbdLayout(context, clipKbdLayout);
|
||||
if (newValue != clipKbdLayout) {
|
||||
await ref
|
||||
.read(prefProvider)
|
||||
.setString(prefClipKbdLayout, newValue);
|
||||
ref.refresh(prefProvider);
|
||||
}
|
||||
},
|
||||
),
|
||||
const ListTitle('Appearance'),
|
||||
ListTile(
|
||||
title: const Text('App theme'),
|
||||
subtitle: Text(ref.read(themeModeProvider).name),
|
||||
onTap: () async {
|
||||
var newMode = await _selectAppearance(context, themeMode);
|
||||
ref.read(themeModeProvider.notifier).setThemeMode(newMode);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _selectKbdLayout(
|
||||
BuildContext context, String currentKbdLayout) async =>
|
||||
await showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: const Text('Choose keyboard layout'),
|
||||
children: <Widget>[
|
||||
RadioListTile<String>(
|
||||
title: const Text('US'),
|
||||
value: 'US',
|
||||
groupValue: currentKbdLayout,
|
||||
onChanged: (mode) {
|
||||
Navigator.pop(context, 'US');
|
||||
}),
|
||||
RadioListTile<String>(
|
||||
title: const Text('DE'),
|
||||
value: 'DE',
|
||||
groupValue: currentKbdLayout,
|
||||
onChanged: (mode) {
|
||||
Navigator.pop(context, 'DE');
|
||||
}),
|
||||
RadioListTile<String>(
|
||||
title: const Text('DE-CH'),
|
||||
value: 'DE-CH',
|
||||
groupValue: currentKbdLayout,
|
||||
onChanged: (mode) {
|
||||
Navigator.pop(context, 'DE-CH');
|
||||
}),
|
||||
],
|
||||
);
|
||||
}) ??
|
||||
defaultClipKbdLayout;
|
||||
|
||||
Future<ThemeMode> _selectAppearance(
|
||||
BuildContext context, ThemeMode themeMode) async =>
|
||||
await showDialog<ThemeMode>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: const Text('Choose app theme'),
|
||||
children: <Widget>[
|
||||
RadioListTile<ThemeMode>(
|
||||
title: const Text('System default'),
|
||||
value: ThemeMode.system,
|
||||
groupValue: themeMode,
|
||||
onChanged: (mode) {
|
||||
Navigator.pop(context, ThemeMode.system);
|
||||
}),
|
||||
RadioListTile<ThemeMode>(
|
||||
title: const Text('Light mode'),
|
||||
value: ThemeMode.light,
|
||||
groupValue: themeMode,
|
||||
onChanged: (mode) {
|
||||
Navigator.pop(context, ThemeMode.light);
|
||||
}),
|
||||
RadioListTile<ThemeMode>(
|
||||
title: const Text('Dark mode'),
|
||||
value: ThemeMode.dark,
|
||||
groupValue: themeMode,
|
||||
onChanged: (mode) {
|
||||
Navigator.pop(context, ThemeMode.dark);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}) ??
|
||||
ThemeMode.system;
|
||||
}
|
@ -44,6 +44,7 @@ enum Application {
|
||||
}
|
||||
|
||||
Availability getAvailability(YubiKeyData data) {
|
||||
|
||||
if (this == Application.management) {
|
||||
final version = data.info.version;
|
||||
final available = (version.major > 4 || // YK5 and up
|
||||
|
@ -1,9 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../management/views/management_screen.dart';
|
||||
import '../../about_page.dart';
|
||||
import '../../android/views/android_settings_page.dart';
|
||||
import '../../management/views/management_screen.dart';
|
||||
import '../../settings_page.dart';
|
||||
import '../message.dart';
|
||||
import '../models.dart';
|
||||
@ -98,7 +101,9 @@ class MainPageDrawer extends ConsumerWidget {
|
||||
if (shouldPop) nav.pop();
|
||||
showBlurDialog(
|
||||
context: context,
|
||||
builder: (context) => const SettingsPage(),
|
||||
builder: (context) => Platform.isAndroid
|
||||
? const AndroidSettingsPage()
|
||||
: const SettingsPage(),
|
||||
routeSettings: const RouteSettings(name: 'settings'),
|
||||
);
|
||||
},
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@ -34,15 +36,20 @@ class MainPage extends ConsumerWidget {
|
||||
|
||||
final deviceNode = ref.watch(currentDeviceProvider);
|
||||
if (deviceNode == null) {
|
||||
return const MessagePage(message: 'Insert your YubiKey');
|
||||
return MessagePage(message: Platform.isAndroid ? 'Insert or tap your YubiKey' : 'Insert your YubiKey');
|
||||
} else {
|
||||
return ref.watch(currentDeviceDataProvider).when(
|
||||
data: (data) {
|
||||
final app = ref.watch(currentAppProvider);
|
||||
if (app.getAvailability(data) != Availability.enabled) {
|
||||
return const MessagePage(
|
||||
if (app.getAvailability(data) == Availability.unsupported) {
|
||||
return MessagePage(
|
||||
header: 'Application not supported',
|
||||
message: 'The used YubiKey does not support \'${app.name}\' application',
|
||||
);
|
||||
} else if (app.getAvailability(data) != Availability.enabled) {
|
||||
return MessagePage(
|
||||
header: 'Application disabled',
|
||||
message: 'Enable the application on your YubiKey to access',
|
||||
message: 'Enable the \'${app.name}\' application on your YubiKey to access',
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user