mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 00:12:09 +03:00
initial
This commit is contained in:
parent
79de2dd2e1
commit
99e857d690
62
android.yaml
Normal file
62
android.yaml
Normal file
@ -0,0 +1,62 @@
|
||||
name: Android Alpha 2
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ feature/android-native ]
|
||||
pull_request:
|
||||
branches: [ feature/android-native ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Debug apk
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Yubikit Next
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: Yubico/yubikit-android
|
||||
ref: next
|
||||
path: kit
|
||||
|
||||
- name: set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: '11'
|
||||
|
||||
- name: Build Yubikit-android
|
||||
run: ./gradlew --stacktrace check test build javadocJar publishToMavenLocal
|
||||
working-directory: ./kit
|
||||
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v1
|
||||
with:
|
||||
channel: 'beta'
|
||||
- run: |
|
||||
flutter config
|
||||
flutter --version
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
path: 'app'
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
flutter test
|
||||
flutter analyze
|
||||
working-directory: ./app
|
||||
|
||||
- name: Build the App
|
||||
run: flutter build apk --debug
|
||||
working-directory: ./app
|
||||
|
||||
- name: Upload artifacts
|
||||
run: |
|
||||
mkdir artifacts
|
||||
mv build/app/outputs/flutter-apk/app-debug.apk artifacts/yubico-authenticator-alpha2.apk
|
||||
working-directory: ./app
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: yubico-authenticator-alpha2.apk.zip
|
||||
path: app/artifacts/*
|
13
android/.gitignore
vendored
Normal file
13
android/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
82
android/app/build.gradle
Normal file
82
android/app/build.gradle
Normal file
@ -0,0 +1,82 @@
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
localPropertiesFile.withReader('UTF-8') { reader ->
|
||||
localProperties.load(reader)
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: "org.jetbrains.kotlin.plugin.serialization"
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion flutter.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.yubico.authenticator"
|
||||
minSdkVersion project.minSdkVersion
|
||||
targetSdkVersion project.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source '../..'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api "com.yubico.yubikit:android:$project.yubiKitVersion"
|
||||
api "com.yubico.yubikit:management:$project.yubiKitVersion"
|
||||
api "com.yubico.yubikit:oath:$project.yubiKitVersion"
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
|
||||
|
||||
// Lifecycle
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
||||
|
||||
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||
}
|
7
android/app/src/debug/AndroidManifest.xml
Normal file
7
android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.yubico.authenticator">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
49
android/app/src/main/AndroidManifest.xml
Normal file
49
android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,49 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.yubico.authenticator">
|
||||
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.nfc"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
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: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
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||
android:resource="@xml/device_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>
|
@ -0,0 +1,28 @@
|
||||
package com.yubico.authenticator.api
|
||||
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.OperationContext
|
||||
import com.yubico.authenticator.ParameterException
|
||||
|
||||
class AppApiImpl(private val modelView: MainViewModel) : Pigeon.AppApi {
|
||||
|
||||
override fun setContext(subPageIndex: Long?, result: Pigeon.Result<Void>?) {
|
||||
|
||||
result?.run {
|
||||
if (subPageIndex == null) {
|
||||
result.error(ParameterException())
|
||||
return
|
||||
}
|
||||
|
||||
val contextValue = OperationContext.getByValue(subPageIndex)
|
||||
if (contextValue == OperationContext.Invalid) {
|
||||
// returning success is all we can do here
|
||||
result.success(null)
|
||||
return
|
||||
}
|
||||
|
||||
modelView.setContext(contextValue)
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.yubico.authenticator.api
|
||||
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
|
||||
class HDialogApiImpl(private val viewModel : MainViewModel) : Pigeon.HDialogApi {
|
||||
override fun dialogClosed(result: Pigeon.Result<Void>?) {
|
||||
result?.run {
|
||||
viewModel.onDialogClosed(result)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
package com.yubico.authenticator.api
|
||||
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.api.Pigeon.OathApi
|
||||
import com.yubico.authenticator.api.Pigeon.Result
|
||||
|
||||
class OathApiImpl(private val viewModel: MainViewModel) : OathApi {
|
||||
|
||||
override fun reset(result: Result<Void>?) {
|
||||
result?.run {
|
||||
viewModel.resetOathSession(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unlock(
|
||||
password: String?,
|
||||
remember: Boolean?,
|
||||
result: Result<Boolean>?
|
||||
) {
|
||||
result?.run {
|
||||
viewModel.unlockOathSession(password, remember, result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setPassword(
|
||||
newPassword: String?,
|
||||
result: Result<Void>?
|
||||
) {
|
||||
result?.run {
|
||||
viewModel.setOathPassword(null, newPassword, result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun changePassword(
|
||||
currentPassword: String?,
|
||||
newPassword: String?,
|
||||
result: Result<Void>?
|
||||
) {
|
||||
result?.run {
|
||||
viewModel.setOathPassword(currentPassword, newPassword, result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun unsetPassword(currentPassword: String?, result: Result<Void>?) {
|
||||
result?.run {
|
||||
viewModel.unsetOathPassword(currentPassword, result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun forgetPassword(result: Result<Void>?) {
|
||||
result?.run {
|
||||
viewModel.forgetPassword(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addAccount(
|
||||
uri: String?,
|
||||
requireTouch: Boolean?,
|
||||
result: Result<String>?
|
||||
) {
|
||||
result?.run {
|
||||
viewModel.addAccount(uri, requireTouch, result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun renameAccount(uri: String?, name: String?, result: Result<String>?) {
|
||||
result?.run {
|
||||
viewModel.renameCredential(uri, name, null, result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun renameAccountWithIssuer(
|
||||
uri: String?,
|
||||
name: String?,
|
||||
issuer: String?,
|
||||
result: Result<String>?
|
||||
) {
|
||||
result?.run {
|
||||
viewModel.renameCredential(uri, name, issuer, result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteAccount(uri: String?, result: Result<Void>?) {
|
||||
result?.run {
|
||||
viewModel.deleteAccount(uri, result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun refreshCodes(result: Result<String>?) {
|
||||
result?.run {
|
||||
viewModel.refreshOathCodes(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun calculate(uri: String?, result: Result<String>?) {
|
||||
result?.run {
|
||||
viewModel.calculate(uri, result)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,673 @@
|
||||
// Autogenerated from Pigeon (v1.0.19), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
package com.yubico.authenticator.api;
|
||||
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import io.flutter.plugin.common.BasicMessageChannel;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.MessageCodec;
|
||||
import io.flutter.plugin.common.StandardMessageCodec;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
/** Generated class from Pigeon. */
|
||||
@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
|
||||
public class Pigeon {
|
||||
|
||||
public interface Result<T> {
|
||||
void success(T result);
|
||||
void error(Throwable error);
|
||||
}
|
||||
private static class OathApiCodec extends StandardMessageCodec {
|
||||
public static final OathApiCodec INSTANCE = new OathApiCodec();
|
||||
private OathApiCodec() {}
|
||||
}
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
|
||||
public interface OathApi {
|
||||
void reset(Result<Void> result);
|
||||
void unlock(String password, Boolean remember, Result<Boolean> result);
|
||||
void setPassword(String newPassword, Result<Void> result);
|
||||
void changePassword(String currentPassword, String newPassword, Result<Void> result);
|
||||
void unsetPassword(String currentPassword, Result<Void> result);
|
||||
void forgetPassword(Result<Void> result);
|
||||
void addAccount(String uri, Boolean requireTouch, Result<String> result);
|
||||
void renameAccount(String uri, String name, Result<String> result);
|
||||
void renameAccountWithIssuer(String uri, String name, String issuer, Result<String> result);
|
||||
void deleteAccount(String uri, Result<Void> result);
|
||||
void refreshCodes(Result<String> result);
|
||||
void calculate(String uri, Result<String> result);
|
||||
|
||||
/** The codec used by OathApi. */
|
||||
static MessageCodec<Object> getCodec() {
|
||||
return OathApiCodec.INSTANCE;
|
||||
}
|
||||
|
||||
/** Sets up an instance of `OathApi` to handle messages through the `binaryMessenger`. */
|
||||
static void setup(BinaryMessenger binaryMessenger, OathApi api) {
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.reset", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
Result<Void> resultCallback = new Result<Void>() {
|
||||
public void success(Void result) {
|
||||
wrapped.put("result", null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.reset(resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.unlock", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
ArrayList<Object> args = (ArrayList<Object>)message;
|
||||
String passwordArg = (String)args.get(0);
|
||||
if (passwordArg == null) {
|
||||
throw new NullPointerException("passwordArg unexpectedly null.");
|
||||
}
|
||||
Boolean rememberArg = (Boolean)args.get(1);
|
||||
if (rememberArg == null) {
|
||||
throw new NullPointerException("rememberArg unexpectedly null.");
|
||||
}
|
||||
Result<Boolean> resultCallback = new Result<Boolean>() {
|
||||
public void success(Boolean result) {
|
||||
wrapped.put("result", result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.unlock(passwordArg, rememberArg, resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.setPassword", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
ArrayList<Object> args = (ArrayList<Object>)message;
|
||||
String newPasswordArg = (String)args.get(0);
|
||||
if (newPasswordArg == null) {
|
||||
throw new NullPointerException("newPasswordArg unexpectedly null.");
|
||||
}
|
||||
Result<Void> resultCallback = new Result<Void>() {
|
||||
public void success(Void result) {
|
||||
wrapped.put("result", null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.setPassword(newPasswordArg, resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.changePassword", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
ArrayList<Object> args = (ArrayList<Object>)message;
|
||||
String currentPasswordArg = (String)args.get(0);
|
||||
if (currentPasswordArg == null) {
|
||||
throw new NullPointerException("currentPasswordArg unexpectedly null.");
|
||||
}
|
||||
String newPasswordArg = (String)args.get(1);
|
||||
if (newPasswordArg == null) {
|
||||
throw new NullPointerException("newPasswordArg unexpectedly null.");
|
||||
}
|
||||
Result<Void> resultCallback = new Result<Void>() {
|
||||
public void success(Void result) {
|
||||
wrapped.put("result", null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.changePassword(currentPasswordArg, newPasswordArg, resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.unsetPassword", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
ArrayList<Object> args = (ArrayList<Object>)message;
|
||||
String currentPasswordArg = (String)args.get(0);
|
||||
if (currentPasswordArg == null) {
|
||||
throw new NullPointerException("currentPasswordArg unexpectedly null.");
|
||||
}
|
||||
Result<Void> resultCallback = new Result<Void>() {
|
||||
public void success(Void result) {
|
||||
wrapped.put("result", null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.unsetPassword(currentPasswordArg, resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.forgetPassword", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
Result<Void> resultCallback = new Result<Void>() {
|
||||
public void success(Void result) {
|
||||
wrapped.put("result", null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.forgetPassword(resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.addAccount", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
ArrayList<Object> args = (ArrayList<Object>)message;
|
||||
String uriArg = (String)args.get(0);
|
||||
if (uriArg == null) {
|
||||
throw new NullPointerException("uriArg unexpectedly null.");
|
||||
}
|
||||
Boolean requireTouchArg = (Boolean)args.get(1);
|
||||
if (requireTouchArg == null) {
|
||||
throw new NullPointerException("requireTouchArg unexpectedly null.");
|
||||
}
|
||||
Result<String> resultCallback = new Result<String>() {
|
||||
public void success(String result) {
|
||||
wrapped.put("result", result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.addAccount(uriArg, requireTouchArg, resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.renameAccount", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
ArrayList<Object> args = (ArrayList<Object>)message;
|
||||
String uriArg = (String)args.get(0);
|
||||
if (uriArg == null) {
|
||||
throw new NullPointerException("uriArg unexpectedly null.");
|
||||
}
|
||||
String nameArg = (String)args.get(1);
|
||||
if (nameArg == null) {
|
||||
throw new NullPointerException("nameArg unexpectedly null.");
|
||||
}
|
||||
Result<String> resultCallback = new Result<String>() {
|
||||
public void success(String result) {
|
||||
wrapped.put("result", result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.renameAccount(uriArg, nameArg, resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.renameAccountWithIssuer", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
ArrayList<Object> args = (ArrayList<Object>)message;
|
||||
String uriArg = (String)args.get(0);
|
||||
if (uriArg == null) {
|
||||
throw new NullPointerException("uriArg unexpectedly null.");
|
||||
}
|
||||
String nameArg = (String)args.get(1);
|
||||
if (nameArg == null) {
|
||||
throw new NullPointerException("nameArg unexpectedly null.");
|
||||
}
|
||||
String issuerArg = (String)args.get(2);
|
||||
if (issuerArg == null) {
|
||||
throw new NullPointerException("issuerArg unexpectedly null.");
|
||||
}
|
||||
Result<String> resultCallback = new Result<String>() {
|
||||
public void success(String result) {
|
||||
wrapped.put("result", result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.renameAccountWithIssuer(uriArg, nameArg, issuerArg, resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.deleteAccount", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
ArrayList<Object> args = (ArrayList<Object>)message;
|
||||
String uriArg = (String)args.get(0);
|
||||
if (uriArg == null) {
|
||||
throw new NullPointerException("uriArg unexpectedly null.");
|
||||
}
|
||||
Result<Void> resultCallback = new Result<Void>() {
|
||||
public void success(Void result) {
|
||||
wrapped.put("result", null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.deleteAccount(uriArg, resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.refreshCodes", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
Result<String> resultCallback = new Result<String>() {
|
||||
public void success(String result) {
|
||||
wrapped.put("result", result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.refreshCodes(resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.OathApi.calculate", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
ArrayList<Object> args = (ArrayList<Object>)message;
|
||||
String uriArg = (String)args.get(0);
|
||||
if (uriArg == null) {
|
||||
throw new NullPointerException("uriArg unexpectedly null.");
|
||||
}
|
||||
Result<String> resultCallback = new Result<String>() {
|
||||
public void success(String result) {
|
||||
wrapped.put("result", result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.calculate(uriArg, resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static class AppApiCodec extends StandardMessageCodec {
|
||||
public static final AppApiCodec INSTANCE = new AppApiCodec();
|
||||
private AppApiCodec() {}
|
||||
}
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
|
||||
public interface AppApi {
|
||||
void setContext(Long subPageIndex, Result<Void> result);
|
||||
|
||||
/** The codec used by AppApi. */
|
||||
static MessageCodec<Object> getCodec() {
|
||||
return AppApiCodec.INSTANCE;
|
||||
}
|
||||
|
||||
/** Sets up an instance of `AppApi` to handle messages through the `binaryMessenger`. */
|
||||
static void setup(BinaryMessenger binaryMessenger, AppApi api) {
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.AppApi.setContext", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
ArrayList<Object> args = (ArrayList<Object>)message;
|
||||
Number subPageIndexArg = (Number)args.get(0);
|
||||
if (subPageIndexArg == null) {
|
||||
throw new NullPointerException("subPageIndexArg unexpectedly null.");
|
||||
}
|
||||
Result<Void> resultCallback = new Result<Void>() {
|
||||
public void success(Void result) {
|
||||
wrapped.put("result", null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.setContext(subPageIndexArg.longValue(), resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static class FOathApiCodec extends StandardMessageCodec {
|
||||
public static final FOathApiCodec INSTANCE = new FOathApiCodec();
|
||||
private FOathApiCodec() {}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/
|
||||
public static class FOathApi {
|
||||
private final BinaryMessenger binaryMessenger;
|
||||
public FOathApi(BinaryMessenger argBinaryMessenger){
|
||||
this.binaryMessenger = argBinaryMessenger;
|
||||
}
|
||||
public interface Reply<T> {
|
||||
void reply(T reply);
|
||||
}
|
||||
static MessageCodec<Object> getCodec() {
|
||||
return FOathApiCodec.INSTANCE;
|
||||
}
|
||||
|
||||
public void updateSession(String sessionJsonArg, Reply<Void> callback) {
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FOathApi.updateSession", getCodec());
|
||||
channel.send(new ArrayList<Object>(Arrays.asList(sessionJsonArg)), channelReply -> {
|
||||
callback.reply(null);
|
||||
});
|
||||
}
|
||||
public void updateOathCredentials(String credentialListJsonArg, Reply<Void> callback) {
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FOathApi.updateOathCredentials", getCodec());
|
||||
channel.send(new ArrayList<Object>(Arrays.asList(credentialListJsonArg)), channelReply -> {
|
||||
callback.reply(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
private static class FManagementApiCodec extends StandardMessageCodec {
|
||||
public static final FManagementApiCodec INSTANCE = new FManagementApiCodec();
|
||||
private FManagementApiCodec() {}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/
|
||||
public static class FManagementApi {
|
||||
private final BinaryMessenger binaryMessenger;
|
||||
public FManagementApi(BinaryMessenger argBinaryMessenger){
|
||||
this.binaryMessenger = argBinaryMessenger;
|
||||
}
|
||||
public interface Reply<T> {
|
||||
void reply(T reply);
|
||||
}
|
||||
static MessageCodec<Object> getCodec() {
|
||||
return FManagementApiCodec.INSTANCE;
|
||||
}
|
||||
|
||||
public void updateDeviceInfo(String deviceInfoJsonArg, Reply<Void> callback) {
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FManagementApi.updateDeviceInfo", getCodec());
|
||||
channel.send(new ArrayList<Object>(Arrays.asList(deviceInfoJsonArg)), channelReply -> {
|
||||
callback.reply(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
private static class FDialogApiCodec extends StandardMessageCodec {
|
||||
public static final FDialogApiCodec INSTANCE = new FDialogApiCodec();
|
||||
private FDialogApiCodec() {}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/
|
||||
public static class FDialogApi {
|
||||
private final BinaryMessenger binaryMessenger;
|
||||
public FDialogApi(BinaryMessenger argBinaryMessenger){
|
||||
this.binaryMessenger = argBinaryMessenger;
|
||||
}
|
||||
public interface Reply<T> {
|
||||
void reply(T reply);
|
||||
}
|
||||
static MessageCodec<Object> getCodec() {
|
||||
return FDialogApiCodec.INSTANCE;
|
||||
}
|
||||
|
||||
public void showDialogApi(String dialogParametersJsonArg, Reply<Void> callback) {
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FDialogApi.showDialogApi", getCodec());
|
||||
channel.send(new ArrayList<Object>(Arrays.asList(dialogParametersJsonArg)), channelReply -> {
|
||||
callback.reply(null);
|
||||
});
|
||||
}
|
||||
public void closeDialogApi(Reply<Void> callback) {
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.FDialogApi.closeDialogApi", getCodec());
|
||||
channel.send(null, channelReply -> {
|
||||
callback.reply(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
private static class HDialogApiCodec extends StandardMessageCodec {
|
||||
public static final HDialogApiCodec INSTANCE = new HDialogApiCodec();
|
||||
private HDialogApiCodec() {}
|
||||
}
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
|
||||
public interface HDialogApi {
|
||||
void dialogClosed(Result<Void> result);
|
||||
|
||||
/** The codec used by HDialogApi. */
|
||||
static MessageCodec<Object> getCodec() {
|
||||
return HDialogApiCodec.INSTANCE;
|
||||
}
|
||||
|
||||
/** Sets up an instance of `HDialogApi` to handle messages through the `binaryMessenger`. */
|
||||
static void setup(BinaryMessenger binaryMessenger, HDialogApi api) {
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.HDialogApi.dialogClosed", getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler((message, reply) -> {
|
||||
Map<String, Object> wrapped = new HashMap<>();
|
||||
try {
|
||||
Result<Void> resultCallback = new Result<Void>() {
|
||||
public void success(Void result) {
|
||||
wrapped.put("result", null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
public void error(Throwable error) {
|
||||
wrapped.put("error", wrapError(error));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
};
|
||||
|
||||
api.dialogClosed(resultCallback);
|
||||
}
|
||||
catch (Error | RuntimeException exception) {
|
||||
wrapped.put("error", wrapError(exception));
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static Map<String, Object> wrapError(Throwable exception) {
|
||||
Map<String, Object> errorMap = new HashMap<>();
|
||||
errorMap.put("message", exception.toString());
|
||||
errorMap.put("code", exception.getClass().getSimpleName());
|
||||
errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
|
||||
return errorMap;
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.yubico.authenticator.api.AppApiImpl
|
||||
import com.yubico.authenticator.api.HDialogApiImpl
|
||||
import com.yubico.authenticator.api.OathApiImpl
|
||||
import com.yubico.authenticator.api.Pigeon
|
||||
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.core.Logger
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class MainActivity : FlutterFragmentActivity() {
|
||||
private val viewModel: MainViewModel by viewModels()
|
||||
private val nfcConfiguration = NfcConfiguration()
|
||||
|
||||
private var hasNfc by Delegates.notNull<Boolean>()
|
||||
|
||||
private lateinit var yubikit: YubiKitManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// simple logger
|
||||
Logger.setLogger(object : Logger() {
|
||||
override fun logDebug(message: String) {
|
||||
Log.d("yubico-authenticator", message)
|
||||
}
|
||||
|
||||
override fun logError(message: String, throwable: Throwable) {
|
||||
Log.e("yubico-authenticator", message, throwable)
|
||||
}
|
||||
})
|
||||
|
||||
yubikit = YubiKitManager(this)
|
||||
|
||||
viewModel.handleYubiKey.observe(this) {
|
||||
if (it) {
|
||||
yubikit.startUsbDiscovery(UsbConfiguration()) { device ->
|
||||
viewModel.yubiKeyDevice.postValue(device)
|
||||
device.setOnClosed { viewModel.yubiKeyDevice.postValue(null) }
|
||||
}
|
||||
hasNfc = try {
|
||||
yubikit.startNfcDiscovery(nfcConfiguration, this) { device ->
|
||||
viewModel.yubiKeyDevice.apply {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
value = device
|
||||
postValue(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (e: NfcNotAvailable) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
yubikit.stopNfcDiscovery(this)
|
||||
yubikit.stopUsbDiscovery()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.yubiKeyDevice.observe(this) { yubikey ->
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (yubikey != null) {
|
||||
Logger.d("A device was connected: $yubikey")
|
||||
viewModel.yubikeyAttached(yubikey)
|
||||
|
||||
} else {
|
||||
Logger.d("A device was disconnected")
|
||||
viewModel.yubikeyDetached()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
|
||||
viewModel.setFOathApi(Pigeon.FOathApi(messenger))
|
||||
viewModel.setFManagementApi(Pigeon.FManagementApi(messenger))
|
||||
viewModel.setFDialogApi(Pigeon.FDialogApi(messenger))
|
||||
Pigeon.OathApi.setup(messenger, OathApiImpl(viewModel))
|
||||
Pigeon.AppApi.setup(messenger, AppApiImpl(viewModel))
|
||||
Pigeon.HDialogApi.setup(messenger, HDialogApiImpl(viewModel))
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,523 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.yubico.authenticator.api.Pigeon
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.Logger
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import com.yubico.yubikit.management.ManagementSession
|
||||
import com.yubico.yubikit.management.ManagementSession.FEATURE_DEVICE_INFO
|
||||
import com.yubico.yubikit.oath.CredentialData
|
||||
import com.yubico.yubikit.oath.OathSession
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URI
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
data class YubiKeyAction(
|
||||
val message: String,
|
||||
val action: suspend (Result<YubiKeyDevice, Exception>) -> Unit
|
||||
)
|
||||
|
||||
enum class OperationContext(val value: Long) {
|
||||
Oath(0), Yubikey(1), Invalid(-1);
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: Long) = values().firstOrNull { it.value == value } ?: Invalid
|
||||
}
|
||||
}
|
||||
|
||||
class ParameterException : Exception("Invalid parameters")
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
|
||||
private val _handleYubiKey = MutableLiveData(true)
|
||||
val handleYubiKey: LiveData<Boolean> = _handleYubiKey
|
||||
|
||||
val yubiKeyDevice = MutableLiveData<YubiKeyDevice?>()
|
||||
private var isUsbKeyConnected: Boolean = false
|
||||
|
||||
private var _oathSessionPassword: CharArray? = null
|
||||
|
||||
private var _operationContext = OperationContext.Oath
|
||||
|
||||
private lateinit var _fOathApi: Pigeon.FOathApi
|
||||
private lateinit var _fManagementApi: Pigeon.FManagementApi
|
||||
private lateinit var _fDialogApi: Pigeon.FDialogApi
|
||||
|
||||
fun setContext(value: OperationContext) {
|
||||
_operationContext = value
|
||||
Logger.d("Operation context is now $_operationContext")
|
||||
}
|
||||
|
||||
fun setFOathApi(oathApi: Pigeon.FOathApi) {
|
||||
_fOathApi = oathApi
|
||||
}
|
||||
|
||||
fun setFManagementApi(managementApi: Pigeon.FManagementApi) {
|
||||
_fManagementApi = managementApi
|
||||
}
|
||||
|
||||
fun setFDialogApi(dialogApi: Pigeon.FDialogApi) {
|
||||
_fDialogApi = dialogApi
|
||||
}
|
||||
|
||||
private suspend fun sendDeviceInfo(device: YubiKeyDevice) {
|
||||
|
||||
val deviceInfoData = suspendCoroutine<String> {
|
||||
device.requestConnection(SmartCardConnection::class.java) { result ->
|
||||
val managementSession = ManagementSession(result.value)
|
||||
|
||||
if (!managementSession.supports(FEATURE_DEVICE_INFO)) {
|
||||
it.resume("NO_FEATURE_DEVICE_INFO")
|
||||
return@requestConnection
|
||||
}
|
||||
|
||||
try {
|
||||
val deviceInfo = managementSession.deviceInfo
|
||||
val deviceInfoData =
|
||||
SerializeHelpers.serialize(deviceInfo, device is NfcYubiKeyDevice)
|
||||
it.resume(deviceInfoData.toString())
|
||||
} catch (cause: Throwable) {
|
||||
Logger.e("Failed to get device info", cause)
|
||||
it.resumeWithException(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_fManagementApi.updateDeviceInfo(deviceInfoData) {}
|
||||
}
|
||||
|
||||
private suspend fun sendOathInfo(device: YubiKeyDevice) {
|
||||
|
||||
val oathSessionData = suspendCoroutine<String> {
|
||||
device.requestConnection(SmartCardConnection::class.java) { result ->
|
||||
val oathSession = OathSession(result.value)
|
||||
val isRemembered = false
|
||||
val oathSessionData = SerializeHelpers.serialize(oathSession, isRemembered)
|
||||
it.resume(oathSessionData.toString())
|
||||
}
|
||||
}
|
||||
|
||||
_fOathApi.updateSession(oathSessionData) {}
|
||||
}
|
||||
|
||||
private suspend fun sendOathCodes(device: YubiKeyDevice) {
|
||||
val sendOathCodes = suspendCoroutine<String> {
|
||||
device.requestConnection(SmartCardConnection::class.java) { result ->
|
||||
val oathSession = OathSession(result.value)
|
||||
|
||||
val isLocked = isOathSessionLocked(oathSession)
|
||||
if (!isLocked) {
|
||||
|
||||
val codes = SerializeHelpers.serialize(
|
||||
oathSession.deviceId,
|
||||
oathSession.calculateCodes()
|
||||
)
|
||||
it.resume(codes.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_fOathApi.updateOathCredentials(sendOathCodes) {}
|
||||
}
|
||||
|
||||
private val _pendingYubiKeyAction = MutableLiveData<YubiKeyAction?>()
|
||||
private val pendingYubiKeyAction: LiveData<YubiKeyAction?> = _pendingYubiKeyAction
|
||||
|
||||
private suspend fun provideYubiKey(result: Result<YubiKeyDevice, Exception>) =
|
||||
withContext(Dispatchers.IO) {
|
||||
pendingYubiKeyAction.value?.let {
|
||||
_pendingYubiKeyAction.postValue(null)
|
||||
if (!isUsbKeyConnected) {
|
||||
withContext(Dispatchers.Main) {
|
||||
requestHideDialog()
|
||||
}
|
||||
}
|
||||
it.action.invoke(result)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun yubikeyAttached(device: YubiKeyDevice) {
|
||||
|
||||
isUsbKeyConnected = device is UsbYubiKeyDevice
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
if (pendingYubiKeyAction.value != null) {
|
||||
provideYubiKey(Result.success(device))
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
when (_operationContext) {
|
||||
OperationContext.Oath -> {
|
||||
try {
|
||||
sendDeviceInfo(device)
|
||||
} catch (cause: Throwable) {
|
||||
Logger.e("Failed to send device info", cause)
|
||||
}
|
||||
sendOathInfo(device)
|
||||
sendOathCodes(device)
|
||||
}
|
||||
OperationContext.Yubikey -> {
|
||||
sendDeviceInfo(device)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun yubikeyDetached() {
|
||||
|
||||
if (isUsbKeyConnected) {
|
||||
// forget the current password only for usb keys
|
||||
_oathSessionPassword = null
|
||||
_fManagementApi.updateDeviceInfo("") {}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun onDialogClosed(result: Pigeon.Result<Void>) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
provideYubiKey(Result.failure(Exception("User canceled")))
|
||||
result.success(null)
|
||||
} catch (cause: Throwable) {
|
||||
Logger.d("failed")
|
||||
result.error(Exception("Failed to close dialog during User cancel action"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// requests flutter to show a dialog
|
||||
private fun requestShowDialog(message: String) =
|
||||
_fDialogApi.showDialogApi(SerializeHelpers.messageParameters(message).toString())
|
||||
{}
|
||||
|
||||
private fun requestHideDialog() {
|
||||
_fDialogApi.closeDialogApi { }
|
||||
}
|
||||
|
||||
private fun <T> withUnlockedSession(session: OathSession, block: (OathSession) -> T): T {
|
||||
val isLocked = isOathSessionLocked(session)
|
||||
if (isLocked) {
|
||||
throw Exception("Session is locked")
|
||||
}
|
||||
return block(session)
|
||||
}
|
||||
|
||||
private fun getOathCredential(oathSession: OathSession, credentialId: String) =
|
||||
oathSession.credentials.firstOrNull { credential ->
|
||||
(credential != null) && SerializeHelpers.credentialIdAsString(
|
||||
credential.id
|
||||
) == credentialId
|
||||
} ?: throw Exception("Failed to find account to delete")
|
||||
|
||||
|
||||
fun deleteAccount(credentialId: String?, result: Pigeon.Result<Void>) {
|
||||
|
||||
if (credentialId == null) {
|
||||
result.error(ParameterException())
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
useOathSession("Delete account", true) { session ->
|
||||
withUnlockedSession(session) {
|
||||
val credential = getOathCredential(session, credentialId)
|
||||
session.deleteCredential(credential)
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAccount(otpUri: String?, requireTouch: Boolean?, result: Pigeon.Result<String>) {
|
||||
if (otpUri == null || requireTouch == null) {
|
||||
result.error(ParameterException())
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
useOathSession("Add account", true) { session ->
|
||||
withUnlockedSession(session) {
|
||||
val credentialData: CredentialData =
|
||||
CredentialData.parseUri(URI.create(otpUri))
|
||||
|
||||
val jsonResult = SerializeHelpers.serialize(
|
||||
session.deviceId,
|
||||
session.putCredential(credentialData, requireTouch)
|
||||
).toString()
|
||||
result.success(jsonResult)
|
||||
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
result.error(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun renameCredential(
|
||||
credentialId: String?,
|
||||
name: String?,
|
||||
issuer: String?,
|
||||
result: Pigeon.Result<String>
|
||||
) {
|
||||
if (credentialId == null || name == null) {
|
||||
result.error(ParameterException())
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
useOathSession("Rename", true) { session ->
|
||||
withUnlockedSession(session) {
|
||||
val credential = getOathCredential(session, credentialId)
|
||||
|
||||
// rename credential
|
||||
val newCredential =
|
||||
session.renameCredential(credential, name, issuer)
|
||||
|
||||
val resultJson = SerializeHelpers.serialize(
|
||||
session.deviceId,
|
||||
newCredential
|
||||
).toString()
|
||||
|
||||
result.success(resultJson)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
result.error(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setOathPassword(current: String?, password: String?, result: Pigeon.Result<Void>) {
|
||||
if (password == null) {
|
||||
result.error(ParameterException())
|
||||
return
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
useOathSession("Set password", true) { session ->
|
||||
if (session.isAccessKeySet) {
|
||||
if (current == null) {
|
||||
throw Exception("Must provide current password to be able to change it")
|
||||
}
|
||||
// test current password sent by the user
|
||||
if (!session.unlock(current.toCharArray())) {
|
||||
throw Exception("Provided current password is invalid")
|
||||
}
|
||||
}
|
||||
val newPass = password.toCharArray()
|
||||
session.setPassword(newPass)
|
||||
_oathSessionPassword = newPass
|
||||
Logger.d("Successfully set password")
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
result.error(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun unsetOathPassword(currentPassword: String?, result: Pigeon.Result<Void>) {
|
||||
|
||||
if (currentPassword == null) {
|
||||
result.error(ParameterException())
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
useOathSession("Unset password", true) { session ->
|
||||
if (session.isAccessKeySet) {
|
||||
// test current password sent by the user
|
||||
if (session.unlock(currentPassword.toCharArray())) {
|
||||
session.deleteAccessKey()
|
||||
_oathSessionPassword = null
|
||||
Logger.d("Successfully unset password")
|
||||
result.success(null)
|
||||
return@useOathSession
|
||||
}
|
||||
}
|
||||
result.error(Exception("Unset password failed"))
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
result.error(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshOathCodes(result: Pigeon.Result<String>) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (!isUsbKeyConnected) {
|
||||
throw Exception("Cannot refresh for nfc key")
|
||||
}
|
||||
|
||||
useOathSession("Refresh codes", false) {
|
||||
withUnlockedSession(it) { session ->
|
||||
val resultJson = SerializeHelpers.serialize(
|
||||
session.deviceId,
|
||||
session.calculateCodes()
|
||||
).toString()
|
||||
|
||||
result.success(resultJson)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
result.error(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun calculate(credentialId: String?, result: Pigeon.Result<String>) {
|
||||
|
||||
if (credentialId == null) {
|
||||
result.error(ParameterException())
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
useOathSession("Calculate", true) {
|
||||
withUnlockedSession(it) { session ->
|
||||
|
||||
val credential = getOathCredential(session, credentialId)
|
||||
|
||||
val resultJson = SerializeHelpers.serialize(
|
||||
session.calculateCode(credential)
|
||||
).toString()
|
||||
|
||||
result.success(resultJson)
|
||||
}
|
||||
}
|
||||
} catch (cause: Throwable) {
|
||||
result.error(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unlockOathSession(
|
||||
password: String?,
|
||||
remember: Boolean?,
|
||||
result: Pigeon.Result<Boolean>
|
||||
) {
|
||||
|
||||
if (password == null || remember == null) {
|
||||
result.error(ParameterException())
|
||||
return
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
var codes: String? = null
|
||||
useOathSession("Unlocking", true) {
|
||||
_oathSessionPassword = password.toCharArray()
|
||||
val isLocked = isOathSessionLocked(it)
|
||||
|
||||
if (!isLocked) {
|
||||
codes = SerializeHelpers.serialize(
|
||||
it.deviceId,
|
||||
it.calculateCodes()
|
||||
).toString()
|
||||
}
|
||||
|
||||
result.success(!isLocked)
|
||||
}
|
||||
|
||||
codes?.let {
|
||||
_fOathApi.updateOathCredentials(it) {}
|
||||
}
|
||||
|
||||
} catch (cause: Throwable) {
|
||||
result.error(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resetOathSession(result: Pigeon.Result<Void>) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
useOathSession("Reset YubiKey", true) {
|
||||
// note, it is ok to reset locked session
|
||||
it.reset()
|
||||
result.success(null)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
result.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> useOathSession(
|
||||
title: String,
|
||||
queryUserToTap: Boolean,
|
||||
action: (OathSession) -> T
|
||||
) = suspendCoroutine<T> { outer ->
|
||||
if (queryUserToTap && !isUsbKeyConnected) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
requestShowDialog(title)
|
||||
}
|
||||
}
|
||||
_pendingYubiKeyAction.postValue(YubiKeyAction(title) { yubiKey ->
|
||||
outer.resumeWith(runCatching {
|
||||
suspendCoroutine { inner ->
|
||||
yubiKey.value.requestConnection(SmartCardConnection::class.java) {
|
||||
inner.resumeWith(runCatching {
|
||||
action.invoke(OathSession(it.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
yubiKeyDevice.value?.let {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
provideYubiKey(Result.success(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the session cannot be unlocked (either we don't have a password, or the password is incorrect
|
||||
*
|
||||
* returns false if we can unlock the session
|
||||
*/
|
||||
private fun isOathSessionLocked(session: OathSession): Boolean {
|
||||
if (!session.isLocked) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (_oathSessionPassword == null) {
|
||||
return true // we have no password to unlock
|
||||
}
|
||||
|
||||
val unlockSucceed = session.unlock(_oathSessionPassword!!)
|
||||
|
||||
if (unlockSucceed) {
|
||||
return false // we have everything to unlock the session
|
||||
}
|
||||
|
||||
_oathSessionPassword = null // reset the password as well as it did not work
|
||||
return true // the unlock did not work, session is locked
|
||||
}
|
||||
|
||||
fun forgetPassword(result: Pigeon.Result<Void>) {
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import com.yubico.yubikit.core.Transport
|
||||
import com.yubico.yubikit.core.Version
|
||||
import com.yubico.yubikit.management.DeviceConfig
|
||||
import com.yubico.yubikit.management.DeviceInfo
|
||||
import com.yubico.yubikit.oath.Code
|
||||
import com.yubico.yubikit.oath.Credential
|
||||
import com.yubico.yubikit.oath.OathSession
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
class SerializeHelpers {
|
||||
companion object {
|
||||
private fun serialize(config: DeviceConfig) = with(config) {
|
||||
JsonObject(
|
||||
mapOf(
|
||||
"device_flags" to JsonPrimitive(deviceFlags),
|
||||
"challenge_response_timeout" to JsonPrimitive(challengeResponseTimeout),
|
||||
"auto_eject_timeout" to JsonPrimitive(autoEjectTimeout),
|
||||
"enabled_capabilities" to JsonObject(
|
||||
mapOf(
|
||||
"usb" to JsonPrimitive(getEnabledCapabilities(Transport.USB) ?: 0),
|
||||
"nfc" to JsonPrimitive(getEnabledCapabilities(Transport.NFC) ?: 0),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun serialize(version: Version) = with(version) {
|
||||
JsonArray(
|
||||
listOf(
|
||||
JsonPrimitive(major),
|
||||
JsonPrimitive(minor),
|
||||
JsonPrimitive(micro)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun serialize(data: DeviceInfo, isNfcDevice: Boolean) =
|
||||
with(data) {
|
||||
JsonObject(
|
||||
mapOf(
|
||||
"config" to serialize(config),
|
||||
"serial" to JsonPrimitive(serialNumber),
|
||||
"version" to serialize(version),
|
||||
"form_factor" to JsonPrimitive(formFactor.value),
|
||||
"is_locked" to JsonPrimitive(isLocked),
|
||||
"is_sky" to JsonPrimitive(false), // FIXME return correct value
|
||||
"is_fips" to JsonPrimitive(false), // FIXME return correct value
|
||||
"name" to JsonPrimitive("FIXME"), // FIXME return correct value
|
||||
"isNFC" to JsonPrimitive(isNfcDevice),
|
||||
"supported_capabilities" to JsonObject(
|
||||
mapOf(
|
||||
"usb" to JsonPrimitive(getSupportedCapabilities(Transport.USB)),
|
||||
"nfc" to JsonPrimitive(getSupportedCapabilities(Transport.NFC)),
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun serialize(oathSession: OathSession, remembered: Boolean) =
|
||||
JsonObject(
|
||||
mapOf(
|
||||
"deviceId" to JsonPrimitive(oathSession.deviceId),
|
||||
"hasKey" to JsonPrimitive(oathSession.isAccessKeySet),
|
||||
"remembered" to JsonPrimitive(remembered),
|
||||
"locked" to JsonPrimitive(oathSession.isLocked)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
fun serialize(code: Code?) =
|
||||
when (code) {
|
||||
null -> JsonNull
|
||||
else -> JsonObject(
|
||||
mapOf(
|
||||
"value" to JsonPrimitive(code.value),
|
||||
"valid_from" to JsonPrimitive(code.validFrom / 1000),
|
||||
"valid_to" to JsonPrimitive(code.validUntil / 1000)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun credentialIdAsString(id: ByteArray): String = id.joinToString(
|
||||
separator = ""
|
||||
) { b -> "%02x".format(b) }
|
||||
|
||||
fun serialize(deviceId: String, credential: Credential) =
|
||||
JsonObject(
|
||||
mapOf(
|
||||
"id" to JsonPrimitive(
|
||||
credentialIdAsString(credential.id)
|
||||
),
|
||||
"device_id" to JsonPrimitive(deviceId),
|
||||
"issuer" to JsonPrimitive(credential.issuer),
|
||||
"name" to JsonPrimitive(credential.accountName),
|
||||
"oath_type" to JsonPrimitive(credential.oathType.value),
|
||||
"period" to JsonPrimitive(credential.period),
|
||||
"touch_required" to JsonPrimitive(credential.isTouchRequired),
|
||||
)
|
||||
)
|
||||
|
||||
fun serialize(deviceId: String, codes: Map<Credential, Code>) =
|
||||
JsonObject(
|
||||
mapOf(
|
||||
"entries" to JsonArray(
|
||||
codes.map { (credential, code) ->
|
||||
JsonObject(
|
||||
mapOf(
|
||||
"credential" to serialize(deviceId, credential),
|
||||
"code" to serialize(code)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
fun messageParameters(message: String) = JsonObject(
|
||||
mapOf(
|
||||
"message" to JsonPrimitive(message)
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
18
android/app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
4
android/app/src/main/res/values/strings.xml
Normal file
4
android/app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_label">Yubico AuthenticatorNG</string>
|
||||
</resources>
|
18
android/app/src/main/res/values/styles.xml
Normal file
18
android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
Flutter draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
7
android/app/src/main/res/xml/device_filter.xml
Normal file
7
android/app/src/main/res/xml/device_filter.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<usb-device
|
||||
class="11"
|
||||
subclass="0"
|
||||
protocol="0" />
|
||||
</resources>
|
7
android/app/src/profile/AndroidManifest.xml
Normal file
7
android/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.yubico.authenticator">
|
||||
<!-- Flutter needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
42
android/build.gradle
Normal file
42
android/build.gradle
Normal file
@ -0,0 +1,42 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.6.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:1.6.10"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
mavenLocal() // TODO: Remove this before release
|
||||
}
|
||||
|
||||
project.ext {
|
||||
minSdkVersion = 21
|
||||
targetSdkVersion = 31
|
||||
compileSdkVersion = 31
|
||||
buildToolsVersion = "30.0.3"
|
||||
|
||||
yubiKitVersion = "2.1.0-SNAPSHOT"
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.buildDir = '../build'
|
||||
subprojects {
|
||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
3
android/gradle.properties
Normal file
3
android/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
6
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
11
android/settings.gradle
Normal file
11
android/settings.gradle
Normal file
@ -0,0 +1,11 @@
|
||||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
Loading…
Reference in New Issue
Block a user