This commit is contained in:
Adam Velebil 2022-03-04 12:20:01 +01:00
parent 79de2dd2e1
commit 99e857d690
No known key found for this signature in database
GPG Key ID: AC6D6B9D715FC084
28 changed files with 1923 additions and 0 deletions

62
android.yaml Normal file
View 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
View 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
View 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'
}

View 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>

View 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>

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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;
}
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -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)
)
)
}
}

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label">Yubico AuthenticatorNG</string>
</resources>

View 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>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<usb-device
class="11"
subclass="0"
protocol="0" />
</resources>

View 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
View 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
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View 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
View 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"