diff --git a/.gitignore b/.gitignore
index c0dd0b5..96486fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@
.buildlog/
.history
.svn/
+migrate_working_dir/
# IntelliJ related
*.iml
@@ -21,31 +22,9 @@
#.vscode/
# Flutter/Dart/Pub related
-**/ios/Flutter/.last_build_id
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
.dart_tool/
-.vscode/
-.flutter-plugins
-.flutter-plugins-dependencies
.packages
-.pub-cache/
-.pub/
-/build/
-
-# Web related
-lib/generated_plugin_registrant.dart
-
-# Symbolication related
-app.*.symbols
-
-# Obfuscation related
-app.*.map.json
-
-# Android Studio will place build artifacts here
-/android/app/debug
-/android/app/profile
-/android/app/release
-.vscode/launch.json
-ios/Flutter/Release.xcconfig
-ios/Flutter/Debug.xcconfig
-macos/Flutter/Flutter-Debug.xcconfig
-macos/Flutter/Flutter-Release.xcconfig
+build/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b8a74a8..32c840c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,13 @@
Changelog
=========
+#### Version 2.0.0-dev.1 (2023-XX-XX)
+* Add PIV protocol
+* BREAKING CHANGES :
+ * `YubicoService().verifyYubiCloudOTP(otp, yubikeyClientAPIKey, yubikeyClientID);` becomes `Yubidart().otp.verify(otp, yubikeyClientAPIKey, yubikeyClientID);`
+* TODO :
+ * separate connection/authentication/processing actions
+
#### Version 1.0.4 (2022-08-16)
* h param is not good when we receive a '+'
* Update project (dependencies, lints, dart version)
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 543d052..71951b2 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -29,7 +29,7 @@ linter:
- always_put_control_body_on_new_line
# - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219
- always_require_non_null_named_parameters
- - always_specify_types
+ # - always_specify_types
- annotate_overrides
# - avoid_annotating_with_dynamic # conflicts with always_specify_types
- avoid_bool_literals_in_conditional_expressions
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..161bdcd
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.cxx
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..64fbf95
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,51 @@
+group 'net.archethic.yubikit_android'
+version '1.0-SNAPSHOT'
+
+buildscript {
+ ext.kotlin_version = '1.6.10'
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.2.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion 31
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ minSdkVersion 19
+ }
+
+ dependencies {
+ implementation 'com.yubico.yubikit:android:2.1.0'
+ implementation 'com.yubico.yubikit:piv:2.1.0'
+ }
+}
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..38f7627
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1 @@
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..6c62fd4
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1,3 @@
+rootProject.name = 'yubikit_android'
+
+
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0fa7256
--- /dev/null
+++ b/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/android/src/main/kotlin/net/archethic/yubikit_android/YubikitAndroidPlugin.kt b/android/src/main/kotlin/net/archethic/yubikit_android/YubikitAndroidPlugin.kt
new file mode 100644
index 0000000..e91359d
--- /dev/null
+++ b/android/src/main/kotlin/net/archethic/yubikit_android/YubikitAndroidPlugin.kt
@@ -0,0 +1,231 @@
+package net.archethic.yubikit_android
+
+import android.app.Activity
+import android.content.Context
+import android.nfc.NfcAdapter
+import android.util.Log
+import androidx.annotation.NonNull
+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.core.smartcard.ApduException
+import com.yubico.yubikit.core.smartcard.SW.*
+import com.yubico.yubikit.core.smartcard.SmartCardConnection
+import com.yubico.yubikit.piv.*
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.embedding.engine.plugins.activity.ActivityAware
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler
+import io.flutter.plugin.common.MethodChannel.Result
+import java.security.KeyFactory
+import java.security.interfaces.ECPublicKey
+import java.security.spec.X509EncodedKeySpec
+import java.util.*
+
+
+/** YubikitAndroidPlugin */
+class YubikitAndroidPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
+ /// The MethodChannel that will the communication between Flutter and native Android
+ ///
+ /// This local reference serves to register the plugin with the Flutter Engine and unregister it
+ /// when the Flutter Engine is detached from the Activity
+ private lateinit var channel: MethodChannel
+ private lateinit var context: Context
+ private lateinit var activity: Activity
+ private lateinit var yubikitManager: YubiKitManager
+
+ override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ channel = MethodChannel(flutterPluginBinding.binaryMessenger, "net.archethic/yubidart")
+ channel.setMethodCallHandler(this)
+ context = flutterPluginBinding.applicationContext
+ yubikitManager = YubiKitManager(context)
+ }
+
+ override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
+ when (call.method) {
+ "isNfcEnabled" -> {
+ val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(context);
+
+ result.success(adapter != null && adapter.isEnabled());
+ }
+ "pivCalculateSecret" -> {
+ Log.d("PIV Calculate secret", "begin")
+
+ val arguments = call.arguments as? HashMap
+ val pin = arguments?.get("pin") as? String
+ val slot = when (val rawSlot = arguments?.get("slot") as? Int) {
+ null -> null
+ else -> Slot.fromValue(rawSlot)
+ }
+ val peerPublicKey =
+ when (val rawPeerPublicKey = arguments?.get("peerPublicKey") as? ByteArray) {
+ null -> null
+ else -> KeyFactory.getInstance("EC")
+ .generatePublic(X509EncodedKeySpec(rawPeerPublicKey)) as ECPublicKey
+ }
+
+
+ if (slot == null || peerPublicKey == null) {
+ result.error(
+ YubikitError.dataError.code,
+ "Data or format error",
+ call.arguments,
+ )
+ return
+ }
+
+ Log.d("PIV Calculate secret", "arguments parsed")
+ yubikitManager.startNfcDiscovery(NfcConfiguration(), activity) { device ->
+ device.requestConnection(SmartCardConnection::class.java) { connectionResult ->
+ guard(result) {
+ Log.d("PIV Calculate secret", "device discovered")
+
+ val connection = connectionResult.getValue()
+ val piv = PivSession(connection)
+ Log.d("PIV Calculate secret", "piv session ok")
+
+ if (pin != null) {
+ piv.verifyPin(
+ pin.toCharArray()
+ )
+ }
+
+ val secret = piv.calculateSecret(slot, peerPublicKey)
+ Log.d("PIV Calculate secret", "secret calculated : $secret")
+
+ result.success(secret)
+ }
+ }
+ }
+ }
+ "pivGenerateKey" -> {
+ Log.d("AUTHENT START", "GO")
+
+ val arguments = call.arguments as? HashMap
+ val pin = arguments?.get("pin") as? String
+ val managementKey = arguments?.get("managementKey") as? ByteArray
+ val managementKeyType =
+ when (val rawManagementKeyType = arguments?.get("managementKeyType") as? Int) {
+ null -> null
+ else -> ManagementKeyType.fromValue(rawManagementKeyType.toByte())
+ }
+ val slot = when (val rawSlot = arguments?.get("slot") as? Int) {
+ null -> null
+ else -> Slot.fromValue(rawSlot)
+ }
+ val keyType = when (val rawKeyType = arguments?.get("type") as? Int) {
+ null -> null
+ else -> KeyType.fromValue(rawKeyType)
+ }
+ val pinPolicy = when (val rawPinPolicy = arguments?.get("pinPolicy") as? Int) {
+ null -> null
+ else -> PinPolicy.fromValue(rawPinPolicy)
+ }
+ val touchPolicy =
+ when (val rawTouchPolicy = arguments?.get("touchPolicy") as? Int) {
+ null -> null
+ else -> TouchPolicy.fromValue(rawTouchPolicy)
+ }
+
+ if (pin == null || managementKey == null || managementKeyType == null || slot == null || keyType == null || pinPolicy == null || touchPolicy == null) {
+ result.error(
+ YubikitError.dataError.code,
+ "Data or format error",
+ call.arguments,
+ )
+ return
+ }
+
+ Log.d("AUTHENTICATE", "BEFORE")
+
+ yubikitManager.startNfcDiscovery(NfcConfiguration(), activity) { device ->
+ device.requestConnection(SmartCardConnection::class.java) { connectionResult ->
+ guard(result) {
+ val connection = connectionResult.getValue()
+ val piv = PivSession(connection)
+ Log.d("AUTHENTICATE", "GO")
+ piv.authenticate(
+ managementKeyType,
+ managementKey,
+ )
+ piv.verifyPin(
+ pin.toCharArray()
+ )
+ val publicKey = piv.generateKey(
+ slot,
+ keyType,
+ pinPolicy,
+ touchPolicy,
+ )
+
+ Log.d("AUTHENTICATE", "DONE")
+ result.success(publicKey.encoded)
+ }
+ }
+ }
+ }
+ "pivGetCertificate" -> {
+ Log.d("PIV Get Certificate", "Start")
+
+ val arguments = call.arguments as? HashMap
+ val pin = arguments?.get("pin") as? String
+ val slot = when (val rawSlot = arguments?.get("slot") as? Int) {
+ null -> null
+ else -> Slot.fromValue(rawSlot)
+ }
+
+
+ if (pin == null || slot == null) {
+ result.error(
+ YubikitError.dataError.code,
+ "Data or format error",
+ call.arguments,
+ )
+ return
+ }
+
+ Log.d("PIV Get Certificate", "Params parsed")
+
+ yubikitManager.startNfcDiscovery(NfcConfiguration(), activity) { device ->
+ device.requestConnection(SmartCardConnection::class.java) { connectionResult ->
+ guard(result) {
+ val connection = connectionResult.getValue()
+ val piv = PivSession(connection)
+ Log.d("PIV Get Certificate", "GO")
+ piv.verifyPin(
+ pin.toCharArray()
+ )
+ Log.d("PIV Get Certificate", "Authentication OK")
+ val certificate = piv.getCertificate(slot)
+ Log.d("PIV Get Certificate", "DONE")
+ result.success(certificate.encoded)
+ }
+ }
+ }
+ }
+ else -> {
+ result.notImplemented()
+ }
+ }
+ }
+
+ override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
+ channel.setMethodCallHandler(null)
+ }
+
+ override fun onDetachedFromActivity() {
+ }
+
+ override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
+ activity = binding.activity;
+ }
+
+ override fun onAttachedToActivity(binding: ActivityPluginBinding) {
+ activity = binding.activity;
+ }
+
+ override fun onDetachedFromActivityForConfigChanges() {
+ }
+}
diff --git a/android/src/main/kotlin/net/archethic/yubikit_android/YubikitError.kt b/android/src/main/kotlin/net/archethic/yubikit_android/YubikitError.kt
new file mode 100644
index 0000000..8b3b6a6
--- /dev/null
+++ b/android/src/main/kotlin/net/archethic/yubikit_android/YubikitError.kt
@@ -0,0 +1,47 @@
+package net.archethic.yubikit_android
+
+import android.util.Log
+import com.yubico.yubikit.core.smartcard.ApduException
+import com.yubico.yubikit.core.smartcard.SW
+import io.flutter.plugin.common.MethodChannel.Result
+
+import com.yubico.yubikit.piv.InvalidPinException
+import com.yubico.yubikit.piv.PivSession
+import java.util.HashMap
+
+enum class YubikitError(val code: String) {
+ other("OTHER"),
+ dataError("INVALID_DATA"),
+ alreadyConnectedFailure("ALREADY_CONNECTED"),
+ notConnectedFailure("NOT_CONNECTED"),
+ unsupportedOperation("UNSUPPORTED_OPERATION"),
+ invalidPin("INVALID_PIN"),
+ authMethodBlocked("AUTH_METHOD_BLOCKED"),
+ invalidMangementKey("INVALID_MANAGEMENT_KEY"),
+ securityConditionNotSatisfied("SECURITY_CONDITION_NOT_SATISFIED"),
+ deviceError("DEVICE_ERROR"),
+}
+
+fun guard( result: Result, task: () -> Unit) {
+ try {
+ task()
+ } catch (e: Exception) {
+ Log.d("GUARD", "exception", e)
+ val error = when (e) {
+ is InvalidPinException ->
+ YubikitError.invalidPin
+ is ApduException -> when (e.sw){
+ SW.AUTH_METHOD_BLOCKED -> YubikitError.authMethodBlocked
+ SW.SECURITY_CONDITION_NOT_SATISFIED -> YubikitError.securityConditionNotSatisfied
+ else -> YubikitError.deviceError
+ }
+ else -> YubikitError.other
+ }
+
+ result.error(
+ error.code,
+ e.localizedMessage,
+ null
+ )
+ }
+}
diff --git a/example/.gitignore b/example/.gitignore
new file mode 100644
index 0000000..24476c5
--- /dev/null
+++ b/example/.gitignore
@@ -0,0 +1,44 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/example/.metadata b/example/.metadata
new file mode 100644
index 0000000..5651284
--- /dev/null
+++ b/example/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled.
+
+version:
+ revision: 52b3dc25f6471c27b2144594abb11c741cb88f57
+ channel: stable
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57
+ base_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57
+ - platform: ios
+ create_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57
+ base_revision: 52b3dc25f6471c27b2144594abb11c741cb88f57
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/example/README.md b/example/README.md
new file mode 100644
index 0000000..13a5576
--- /dev/null
+++ b/example/README.md
@@ -0,0 +1,16 @@
+# yubikit_android_example
+
+Demonstrates how to use the yubikit_android plugin.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml
new file mode 100644
index 0000000..61b6c4d
--- /dev/null
+++ b/example/analysis_options.yaml
@@ -0,0 +1,29 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at
+ # https://dart-lang.github.io/linter/lints/index.html.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/example/android/.gitignore b/example/android/.gitignore
new file mode 100644
index 0000000..6f56801
--- /dev/null
+++ b/example/android/.gitignore
@@ -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
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
new file mode 100644
index 0000000..c13485e
--- /dev/null
+++ b/example/android/app/build.gradle
@@ -0,0 +1,71 @@
+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 GradleException("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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion flutter.compileSdkVersion
+ ndkVersion flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "net.archethic.yubikit_android_example"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
+ minSdkVersion 19
+ targetSdkVersion flutter.targetSdkVersion
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..35154f9
--- /dev/null
+++ b/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c1e33fb
--- /dev/null
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/kotlin/net/archethic/yubikit_android_example/MainActivity.kt b/example/android/app/src/main/kotlin/net/archethic/yubikit_android_example/MainActivity.kt
new file mode 100644
index 0000000..b8c38ca
--- /dev/null
+++ b/example/android/app/src/main/kotlin/net/archethic/yubikit_android_example/MainActivity.kt
@@ -0,0 +1,6 @@
+package net.archethic.yubikit_android_example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/example/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..35154f9
--- /dev/null
+++ b/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/example/android/build.gradle b/example/android/build.gradle
new file mode 100644
index 0000000..83ae220
--- /dev/null
+++ b/example/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+ ext.kotlin_version = '1.6.10'
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.1.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/example/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..cb24abd
--- /dev/null
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
diff --git a/example/android/settings.gradle b/example/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/example/android/settings.gradle
@@ -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"
diff --git a/example/example.dart b/example/example.dart
deleted file mode 100644
index 753a954..0000000
--- a/example/example.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-import 'package:nfc_manager/nfc_manager.dart';
-import 'package:yubidart/src/model/verification_response.dart';
-import 'package:yubidart/yubidart.dart' show YubicoService;
-
-Future main(List args) async {
- /// Verify if NFC is avalaible
- final bool isAvailable = await NfcManager.instance.isAvailable();
- if (isAvailable) {
- NfcManager.instance.startSession(onDiscovered: (NfcTag tag) async {
- final String otp = YubicoService().getOTPFromYubiKeyNFC(tag);
-
- /// Verify OTP with YubiCloud
- final VerificationResponse verificationResponse = await YubicoService()
- .verifyYubiCloudOTP(otp, 'mG5be6ZJU1qBGz24yPh/ESM3UdU=', '1');
- NfcManager.instance.stopSession();
- if (verificationResponse.status == 'OK') {
- print('OTP valid');
- } else {
- print('Error : ${verificationResponse.status}');
- }
- });
- }
-}
diff --git a/example/ios/.gitignore b/example/ios/.gitignore
new file mode 100644
index 0000000..7a7f987
--- /dev/null
+++ b/example/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..9625e10
--- /dev/null
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 11.0
+
+
diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..ec97fc6
--- /dev/null
+++ b/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..c4855bf
--- /dev/null
+++ b/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/example/ios/Podfile b/example/ios/Podfile
new file mode 100644
index 0000000..88359b2
--- /dev/null
+++ b/example/ios/Podfile
@@ -0,0 +1,41 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '11.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
new file mode 100644
index 0000000..e7c9182
--- /dev/null
+++ b/example/ios/Podfile.lock
@@ -0,0 +1,39 @@
+PODS:
+ - ASN1Decoder (1.8.0)
+ - Flutter (1.0.0)
+ - nfc_manager (0.0.1):
+ - Flutter
+ - yubidart (0.0.1):
+ - ASN1Decoder
+ - Flutter
+ - YubiKit (~> 4.2.0)
+ - YubiKit (4.2.0)
+
+DEPENDENCIES:
+ - Flutter (from `Flutter`)
+ - nfc_manager (from `.symlinks/plugins/nfc_manager/ios`)
+ - yubidart (from `.symlinks/plugins/yubidart/ios`)
+
+SPEC REPOS:
+ trunk:
+ - ASN1Decoder
+ - YubiKit
+
+EXTERNAL SOURCES:
+ Flutter:
+ :path: Flutter
+ nfc_manager:
+ :path: ".symlinks/plugins/nfc_manager/ios"
+ yubidart:
+ :path: ".symlinks/plugins/yubidart/ios"
+
+SPEC CHECKSUMS:
+ ASN1Decoder: 6110fdeacfdb41559b1481457a1645be716610aa
+ Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
+ nfc_manager: d7da7cb781f7744b94df5fe9dbca904ac4a0939e
+ yubidart: 0393c4728755f4e281dd0adb82e2f2b0168e2d57
+ YubiKit: bde189dbaddc29e41dbda9e2c55ec5a79d38c654
+
+PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
+
+COCOAPODS: 1.11.3
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..57e587b
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,560 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 51;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 6FD71207C9A106EE221CB978 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7BD85E78878E2EE1552705AE /* Pods_Runner.framework */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 7BD85E78878E2EE1552705AE /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 874F9CA56F31B1CC2DDAC512 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ E1BCA5346EFAFBBF84388773 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ EA447DAE29390F9800275EBA /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = ""; };
+ EAD32A502939114C00DA5537 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; };
+ EAD32A512939115100DA5537 /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = ""; };
+ F1B37718846C217EE78236EE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6FD71207C9A106EE221CB978 /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 2E5D19F46FB1853B8B254499 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 7BD85E78878E2EE1552705AE /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 705CEA40D8DCBEDA8EC985B1 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ F1B37718846C217EE78236EE /* Pods-Runner.debug.xcconfig */,
+ 874F9CA56F31B1CC2DDAC512 /* Pods-Runner.release.xcconfig */,
+ E1BCA5346EFAFBBF84388773 /* Pods-Runner.profile.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 705CEA40D8DCBEDA8EC985B1 /* Pods */,
+ 2E5D19F46FB1853B8B254499 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ EAD32A512939115100DA5537 /* RunnerRelease.entitlements */,
+ EAD32A502939114C00DA5537 /* RunnerDebug.entitlements */,
+ EA447DAE29390F9800275EBA /* RunnerProfile.entitlements */,
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 961A1D08863D98BF3414BE9E /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ CF8DFA8082070D652B14FA6C /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1300;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 961A1D08863D98BF3414BE9E /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+ CF8DFA8082070D652B14FA6C /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = T3ZV8MV4P2;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = net.archethic.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = T3ZV8MV4P2;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = net.archethic.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = T3ZV8MV4P2;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = net.archethic.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..c87d15a
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..28c6bf0
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..2ccbfd9
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..f091b6b
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cde121
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d0ef06e
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..dcdc230
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..2ccbfd9
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..c8f9ed8
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..a6d6b86
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..a6d6b86
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..75b2d16
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..c4df70d
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..6a84f41
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..d0e1f58
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist
new file mode 100644
index 0000000..2c1e6e0
--- /dev/null
+++ b/example/ios/Runner/Info.plist
@@ -0,0 +1,62 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Example
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ example
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
+
+ NFCReaderUsageDescription
+ The application needs access to NFC reading to communicate with your YubiKey.
+ com.apple.developer.nfc.readersession.iso7816.select-identifiers
+
+ A000000527471117
+ A0000006472F0001
+ A0000005272101
+ A000000308
+ A000000527200101
+
+
+
diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/example/ios/Runner/RunnerDebug.entitlements b/example/ios/Runner/RunnerDebug.entitlements
new file mode 100644
index 0000000..e121fd5
--- /dev/null
+++ b/example/ios/Runner/RunnerDebug.entitlements
@@ -0,0 +1,11 @@
+
+
+
+
+ com.apple.developer.nfc.readersession.formats
+
+ NDEF
+ TAG
+
+
+
diff --git a/example/ios/Runner/RunnerProfile.entitlements b/example/ios/Runner/RunnerProfile.entitlements
new file mode 100644
index 0000000..e121fd5
--- /dev/null
+++ b/example/ios/Runner/RunnerProfile.entitlements
@@ -0,0 +1,11 @@
+
+
+
+
+ com.apple.developer.nfc.readersession.formats
+
+ NDEF
+ TAG
+
+
+
diff --git a/example/ios/Runner/RunnerRelease.entitlements b/example/ios/Runner/RunnerRelease.entitlements
new file mode 100644
index 0000000..e121fd5
--- /dev/null
+++ b/example/ios/Runner/RunnerRelease.entitlements
@@ -0,0 +1,11 @@
+
+
+
+
+ com.apple.developer.nfc.readersession.formats
+
+ NDEF
+ TAG
+
+
+
diff --git a/example/lib/components/action_button.dart b/example/lib/components/action_button.dart
new file mode 100644
index 0000000..091fb0b
--- /dev/null
+++ b/example/lib/components/action_button.dart
@@ -0,0 +1,78 @@
+import 'dart:developer';
+
+import 'package:flutter/material.dart';
+import 'package:yubidart/yubidart.dart';
+import 'package:yubikit_android_example/components/snackbar.dart';
+import 'package:yubikit_android_example/failure_message.dart';
+
+class ActionButton extends StatefulWidget {
+ const ActionButton({
+ super.key,
+ required this.text,
+ required this.onPressed,
+ });
+ final String text;
+ final Future Function() onPressed;
+
+ @override
+ State createState() => _ActionButtonState();
+}
+
+class _ActionButtonState extends State {
+ bool isOperationRunning = false;
+
+ @override
+ Widget build(BuildContext context) {
+ if (isOperationRunning) {
+ return const TextButton(
+ onPressed: null,
+ child: CircularProgressIndicator(),
+ );
+ }
+
+ return TextButton(
+ onPressed: _runOperation,
+ child: Text(widget.text),
+ );
+ }
+
+ void _showError(BuildContext context, String message) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ ResultSnackbar.error(message),
+ );
+ }
+
+ void _showSuccess(BuildContext context, String message) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ ResultSnackbar.success(message),
+ );
+ }
+
+ Future _runOperation() async {
+ if (isOperationRunning) return;
+ setState(() {
+ isOperationRunning = true;
+ });
+
+ try {
+ final resultMessage = await widget.onPressed();
+ log('Success : $resultMessage');
+ if (mounted) {
+ _showSuccess(context, resultMessage);
+ }
+ } on YKFailure catch (e) {
+ log('YKFailure : ${e.message}');
+ if (mounted) {
+ _showError(context, e.message);
+ }
+ } catch (e) {
+ log('Failure : ${e.toString()}');
+ if (mounted) {
+ _showError(context, e.toString());
+ }
+ }
+ setState(() {
+ isOperationRunning = false;
+ });
+ }
+}
diff --git a/example/lib/components/capabilities_text.dart b/example/lib/components/capabilities_text.dart
new file mode 100644
index 0000000..d38f1c9
--- /dev/null
+++ b/example/lib/components/capabilities_text.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:yubidart/yubidart.dart';
+
+class CapabilitiesText extends StatelessWidget {
+ const CapabilitiesText({
+ super.key,
+ required this.yubikitPlugin,
+ });
+ final Yubidart yubikitPlugin;
+
+ @override
+ Widget build(BuildContext context) {
+ return FutureBuilder(
+ future: capabilitiesString(),
+ builder: (context, snapshot) {
+ if (snapshot.hasData) {
+ return Text('With capabilities : ${snapshot.data}');
+ }
+ return const Text('Loading capabilities ...');
+ },
+ );
+ }
+
+ Future capabilitiesString() async {
+ try {
+ final capabilities = await yubikitPlugin.general.deviceCapabilities;
+ return 'nfc : ${capabilities.nfc}, wired : ${capabilities.wired}';
+ } on PlatformException {
+ return 'Failed to get device capabilities';
+ }
+ }
+}
diff --git a/example/lib/components/generate_key_button.dart b/example/lib/components/generate_key_button.dart
new file mode 100644
index 0000000..e5502a0
--- /dev/null
+++ b/example/lib/components/generate_key_button.dart
@@ -0,0 +1,31 @@
+import 'package:flutter/material.dart';
+import 'package:yubidart/yubidart.dart';
+import 'package:yubikit_android_example/components/action_button.dart';
+
+class GenerateKeyButton extends StatelessWidget {
+ const GenerateKeyButton({
+ super.key,
+ required this.yubikitPlugin,
+ });
+
+ final Yubidart yubikitPlugin;
+
+ @override
+ Widget build(BuildContext context) => ActionButton(
+ text: 'Generate key',
+ onPressed: () async {
+ final publicKey = await yubikitPlugin.piv.generateKey(
+ pin: "123456",
+ managementKey: PivManagementKey.fromString(
+ "010203040506070801020304050607080102030405060708",
+ keyType: PivManagementKeyType.tripleDES,
+ ),
+ pinPolicy: PivPinPolicy.defaultPolicy,
+ type: PivKeyType.eccp256,
+ touchPolicy: PivTouchPolicy.defaultPolicy,
+ slot: PivSlot.signature,
+ );
+ return publicKey.toString();
+ },
+ );
+}
diff --git a/example/lib/components/piv_calculate_secret_button.dart b/example/lib/components/piv_calculate_secret_button.dart
new file mode 100644
index 0000000..a42f69a
--- /dev/null
+++ b/example/lib/components/piv_calculate_secret_button.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/material.dart';
+import 'package:yubidart/yubidart.dart';
+import 'package:yubikit_android_example/components/action_button.dart';
+
+class PivCalculateSecretButton extends StatelessWidget {
+ const PivCalculateSecretButton({
+ super.key,
+ required this.yubikitPlugin,
+ });
+
+ final Yubidart yubikitPlugin;
+
+ @override
+ Widget build(BuildContext context) => ActionButton(
+ text: 'Calculate secret',
+ onPressed: () async {
+ final secret = await yubikitPlugin.piv.calculateSecret(
+ slot: PivSlot.authentication,
+ pin: "123456",
+ peerPublicKey: """
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAElqeFrBCjtonol5ksKYCuXf+alUTI
+60I0O7layDn75ar9UnvTnCmywrsp/434Mg5R7+02W1glNilGvW4pHfUWNA==
+-----END PUBLIC KEY-----
+""",
+ );
+ return secret.toString();
+ },
+ );
+}
diff --git a/example/lib/components/piv_read_cert_button.dart b/example/lib/components/piv_read_cert_button.dart
new file mode 100644
index 0000000..8d28259
--- /dev/null
+++ b/example/lib/components/piv_read_cert_button.dart
@@ -0,0 +1,24 @@
+import 'package:flutter/material.dart';
+import 'package:yubidart/yubidart.dart';
+import 'package:yubikit_android_example/components/action_button.dart';
+
+class PivReadCertButton extends StatelessWidget {
+ const PivReadCertButton({
+ super.key,
+ required this.yubikitPlugin,
+ });
+
+ final Yubidart yubikitPlugin;
+
+ @override
+ Widget build(BuildContext context) => ActionButton(
+ text: 'Read certificate',
+ onPressed: () async {
+ final publicKey = await yubikitPlugin.piv.getCertificate(
+ pin: "123456",
+ slot: PivSlot.signature,
+ );
+ return publicKey.toString();
+ },
+ );
+}
diff --git a/example/lib/components/snackbar.dart b/example/lib/components/snackbar.dart
new file mode 100644
index 0000000..70a9191
--- /dev/null
+++ b/example/lib/components/snackbar.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+
+class ResultSnackbar extends SnackBar {
+ ResultSnackbar({
+ super.key,
+ required String prefix,
+ required String message,
+ required Color color,
+ }) : super(
+ duration: const Duration(seconds: 15),
+ backgroundColor: color,
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(prefix),
+ Text(message),
+ ],
+ ),
+ );
+
+ factory ResultSnackbar.success(String message) => ResultSnackbar(
+ prefix: "Success :",
+ message: message,
+ color: Colors.green,
+ );
+
+ factory ResultSnackbar.error(String message) => ResultSnackbar(
+ prefix: "Error :",
+ message: message,
+ color: Colors.red,
+ );
+}
diff --git a/example/lib/failure_message.dart b/example/lib/failure_message.dart
new file mode 100644
index 0000000..5d4179b
--- /dev/null
+++ b/example/lib/failure_message.dart
@@ -0,0 +1,23 @@
+import 'package:yubidart/yubidart.dart';
+
+extension YKFailureMessageExt on YKFailure {
+ String get message {
+ if (this is InvalidPin) {
+ return "Invalid pin. ${(this as InvalidPin).remainingRetries} tries remaining.";
+ }
+
+ if (this is InvalidPIVManagementKey) {
+ return "Invalid management key. ${(this as InvalidPIVManagementKey).message}.";
+ }
+
+ if (this is UnsupportedOperation) {
+ return "Unsupported operation";
+ }
+
+ if (this is NotConnectedFailure) {
+ return "Connection to Yubikey failed";
+ }
+
+ return "An error occured";
+ }
+}
diff --git a/example/lib/main.dart b/example/lib/main.dart
new file mode 100644
index 0000000..73f5024
--- /dev/null
+++ b/example/lib/main.dart
@@ -0,0 +1,43 @@
+import 'package:flutter/material.dart';
+import 'package:yubidart/yubidart.dart';
+import 'package:yubikit_android_example/components/capabilities_text.dart';
+import 'package:yubikit_android_example/components/generate_key_button.dart';
+import 'package:yubikit_android_example/components/piv_calculate_secret_button.dart';
+import 'package:yubikit_android_example/components/piv_read_cert_button.dart';
+
+void main() {
+ runApp(const MyApp());
+}
+
+class MyApp extends StatefulWidget {
+ const MyApp({super.key});
+
+ @override
+ State createState() => _MyAppState();
+}
+
+class _MyAppState extends State {
+ final _yubikitPlugin = Yubidart();
+
+ @override
+ Widget build(BuildContext context) => MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('Yubikit example app'),
+ ),
+ body: Center(
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: CapabilitiesText(yubikitPlugin: _yubikitPlugin),
+ ),
+ GenerateKeyButton(yubikitPlugin: _yubikitPlugin),
+ PivReadCertButton(yubikitPlugin: _yubikitPlugin),
+ PivCalculateSecretButton(yubikitPlugin: _yubikitPlugin),
+ ],
+ ),
+ ),
+ ),
+ );
+}
diff --git a/pubspec.lock b/example/pubspec.lock
similarity index 51%
rename from pubspec.lock
rename to example/pubspec.lock
index 7888349..17b6a0b 100644
--- a/pubspec.lock
+++ b/example/pubspec.lock
@@ -1,34 +1,13 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
- _fe_analyzer_shared:
- dependency: transitive
- description:
- name: _fe_analyzer_shared
- url: "https://pub.dartlang.org"
- source: hosted
- version: "46.0.0"
- analyzer:
- dependency: transitive
- description:
- name: analyzer
- url: "https://pub.dartlang.org"
- source: hosted
- version: "4.6.0"
- args:
- dependency: transitive
- description:
- name: args
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.3.0"
async:
dependency: transitive
description:
name: async
url: "https://pub.dartlang.org"
source: hosted
- version: "2.8.2"
+ version: "2.9.0"
boolean_selector:
dependency: transitive
description:
@@ -42,14 +21,14 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
- version: "1.2.0"
- charcode:
+ version: "1.2.1"
+ clock:
dependency: transitive
description:
- name: charcode
+ name: clock
url: "https://pub.dartlang.org"
source: hosted
- version: "1.3.1"
+ version: "1.1.1"
collection:
dependency: transitive
description:
@@ -57,36 +36,43 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.16.0"
- convert:
+ crypto:
dependency: transitive
description:
- name: convert
+ name: crypto
url: "https://pub.dartlang.org"
source: hosted
- version: "3.0.1"
- coverage:
+ version: "3.0.2"
+ cryptography:
dependency: transitive
description:
- name: coverage
+ name: cryptography
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.3"
- crypto:
+ version: "2.0.5"
+ cupertino_icons:
dependency: "direct main"
description:
- name: crypto
+ name: cupertino_icons
url: "https://pub.dartlang.org"
source: hosted
- version: "3.0.2"
- file:
+ version: "1.0.5"
+ fake_async:
dependency: transitive
description:
- name: file
+ name: fake_async
url: "https://pub.dartlang.org"
source: hosted
- version: "6.1.2"
- flutter:
+ version: "1.3.1"
+ fixnum:
dependency: transitive
+ description:
+ name: fixnum
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1"
+ flutter:
+ dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
@@ -97,207 +83,121 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
- frontend_server_client:
- dependency: transitive
- description:
- name: frontend_server_client
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.1.2"
- glob:
- dependency: transitive
- description:
- name: glob
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.0.2"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
http:
- dependency: "direct main"
+ dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.5"
- http_multi_server:
- dependency: transitive
- description:
- name: http_multi_server
- url: "https://pub.dartlang.org"
- source: hosted
- version: "3.0.1"
http_parser:
dependency: transitive
description:
name: http_parser
url: "https://pub.dartlang.org"
source: hosted
- version: "4.0.0"
- import_sorter:
- dependency: "direct dev"
- description:
- name: import_sorter
- url: "https://pub.dartlang.org"
- source: hosted
- version: "4.6.0"
- io:
- dependency: transitive
- description:
- name: io
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.0.3"
+ version: "4.0.2"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
- version: "0.6.3"
- lints:
- dependency: transitive
+ version: "0.6.5"
+ jwk:
+ dependency: "direct main"
description:
- name: lints
+ name: jwk
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.0"
- logging:
+ version: "0.1.1"
+ lints:
dependency: transitive
description:
- name: logging
+ name: lints
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.2"
+ version: "2.0.1"
matcher:
dependency: transitive
description:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
- version: "0.12.11"
+ version: "0.12.12"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
- version: "0.1.4"
+ version: "0.1.5"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
- version: "1.7.0"
- mime:
- dependency: transitive
- description:
- name: mime
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.0.1"
+ version: "1.8.0"
nfc_manager:
- dependency: "direct main"
- description:
- name: nfc_manager
- url: "https://pub.dartlang.org"
- source: hosted
- version: "3.1.1"
- node_preamble:
dependency: transitive
description:
- name: node_preamble
+ name: nfc_manager
url: "https://pub.dartlang.org"
source: hosted
- version: "2.0.1"
+ version: "3.2.0"
nonce:
- dependency: "direct main"
+ dependency: transitive
description:
name: nonce
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
- package_config:
- dependency: transitive
- description:
- name: package_config
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.0.2"
path:
dependency: transitive
description:
name: path
url: "https://pub.dartlang.org"
source: hosted
- version: "1.8.1"
- pool:
- dependency: transitive
- description:
- name: pool
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.5.0"
- pub_semver:
- dependency: transitive
- description:
- name: pub_semver
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.1.0"
- shelf:
- dependency: transitive
- description:
- name: shelf
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.2.0"
- shelf_packages_handler:
- dependency: transitive
+ version: "1.8.2"
+ pem:
+ dependency: "direct main"
description:
- name: shelf_packages_handler
+ name: pem
url: "https://pub.dartlang.org"
source: hosted
- version: "3.0.0"
- shelf_static:
+ version: "2.0.1"
+ petitparser:
dependency: transitive
description:
- name: shelf_static
+ name: petitparser
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.0"
- shelf_web_socket:
+ version: "5.1.0"
+ plugin_platform_interface:
dependency: transitive
description:
- name: shelf_web_socket
+ name: plugin_platform_interface
url: "https://pub.dartlang.org"
source: hosted
- version: "1.0.1"
+ version: "2.1.3"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
- source_map_stack_trace:
- dependency: transitive
- description:
- name: source_map_stack_trace
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.1.0"
- source_maps:
- dependency: transitive
- description:
- name: source_maps
- url: "https://pub.dartlang.org"
- source: hosted
- version: "0.10.10"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
- version: "1.8.1"
+ version: "1.9.0"
stack_trace:
dependency: transitive
description:
@@ -318,21 +218,14 @@ packages:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
- version: "1.1.0"
+ version: "1.1.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
- version: "1.2.0"
- test:
- dependency: "direct dev"
- description:
- name: test
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.21.4"
+ version: "1.2.1"
test_api:
dependency: transitive
description:
@@ -340,27 +233,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.12"
- test_core:
- dependency: transitive
- description:
- name: test_core
- url: "https://pub.dartlang.org"
- source: hosted
- version: "0.4.16"
- tint:
- dependency: transitive
- description:
- name: tint
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.0.0"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
- version: "1.3.0"
+ version: "1.3.1"
vector_math:
dependency: transitive
description:
@@ -368,41 +247,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
- vm_service:
- dependency: transitive
- description:
- name: vm_service
- url: "https://pub.dartlang.org"
- source: hosted
- version: "7.5.0"
- watcher:
- dependency: transitive
- description:
- name: watcher
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.0.1"
- web_socket_channel:
- dependency: transitive
- description:
- name: web_socket_channel
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.1.0"
- webkit_inspection_protocol:
- dependency: transitive
- description:
- name: webkit_inspection_protocol
- url: "https://pub.dartlang.org"
- source: hosted
- version: "1.0.0"
- yaml:
- dependency: transitive
+ yubidart:
+ dependency: "direct main"
description:
- name: yaml
- url: "https://pub.dartlang.org"
- source: hosted
- version: "3.1.0"
+ path: ".."
+ relative: true
+ source: path
+ version: "2.0.0-dev.1"
sdks:
- dart: ">=2.17.0 <3.0.0"
- flutter: ">=1.20.0"
+ dart: ">=2.18.4 <3.0.0"
+ flutter: ">=2.5.0"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
new file mode 100644
index 0000000..a6623a0
--- /dev/null
+++ b/example/pubspec.yaml
@@ -0,0 +1,85 @@
+name: yubikit_android_example
+description: Demonstrates how to use the yubikit_android plugin.
+
+# The following line prevents the package from being accidentally published to
+# pub.dev using `flutter pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+environment:
+ sdk: '>=2.18.4 <3.0.0'
+
+# Dependencies specify other packages that your package needs in order to work.
+# To automatically upgrade your package dependencies to the latest versions
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
+# dependencies can be manually updated by changing the version numbers below to
+# the latest version available on pub.dev. To see which dependencies have newer
+# versions available, run `flutter pub outdated`.
+dependencies:
+ flutter:
+ sdk: flutter
+
+ yubidart:
+ path: ../
+
+ # cryptography: ^2.0.5
+
+
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: ^1.0.2
+ jwk: ^0.1.1
+
+ pem: ^2.0.1
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+ # The "flutter_lints" package below contains a set of recommended lints to
+ # encourage good coding practices. The lint set provided by the package is
+ # activated in the `analysis_options.yaml` file located at the root of your
+ # package. See that file for information about deactivating specific lint
+ # rules and activating additional ones.
+ flutter_lints: ^2.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+
+ # The following line ensures that the Material Icons font is
+ # included with your application, so that you can use the icons in
+ # the material Icons class.
+ uses-material-design: true
+
+ # To add assets to your application, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/assets-and-images/#from-packages
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts from package dependencies,
+ # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
new file mode 100644
index 0000000..aed307e
--- /dev/null
+++ b/example/test/widget_test.dart
@@ -0,0 +1,27 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:yubikit_android_example/main.dart';
+
+void main() {
+ testWidgets('Verify Platform version', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(const MyApp());
+
+ // Verify that platform version is retrieved.
+ expect(
+ find.byWidgetPredicate(
+ (Widget widget) => widget is Text &&
+ widget.data!.startsWith('Running on:'),
+ ),
+ findsOneWidget,
+ );
+ });
+}
diff --git a/ios/.gitignore b/ios/.gitignore
new file mode 100644
index 0000000..0c88507
--- /dev/null
+++ b/ios/.gitignore
@@ -0,0 +1,38 @@
+.idea/
+.vagrant/
+.sconsign.dblite
+.svn/
+
+.DS_Store
+*.swp
+profile
+
+DerivedData/
+build/
+GeneratedPluginRegistrant.h
+GeneratedPluginRegistrant.m
+
+.generated/
+
+*.pbxuser
+*.mode1v3
+*.mode2v3
+*.perspectivev3
+
+!default.pbxuser
+!default.mode1v3
+!default.mode2v3
+!default.perspectivev3
+
+xcuserdata
+
+*.moved-aside
+
+*.pyc
+*sync/
+Icon?
+.tags*
+
+/Flutter/Generated.xcconfig
+/Flutter/ephemeral/
+/Flutter/flutter_export_environment.sh
\ No newline at end of file
diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/ios/Classes/Connection.swift b/ios/Classes/Connection.swift
new file mode 100644
index 0000000..c448cea
--- /dev/null
+++ b/ios/Classes/Connection.swift
@@ -0,0 +1,120 @@
+//
+// Connection.swift
+// yubikit_ios
+//
+// Created by charly on 19/12/2022.
+//
+
+import Foundation
+import Flutter
+import UIKit
+import YubiKit
+
+
+enum YubikeyConnectionType: UInt8 {
+ case NFC = 0b00000001
+ case Accessory = 0b0000010
+}
+
+extension UInt8 {
+ func isEnabled(_ connectionType: YubikeyConnectionType) -> Bool {
+ return self & connectionType.rawValue != 0
+ }
+}
+
+class YubiKeyConnection: NSObject {
+ var connectionType: UInt8
+ var activeConnection: YKFConnectionProtocol?
+ var connectionCallback: ((_ connection: YKFConnectionProtocol) -> Void)?
+ var connectionErrorCallback: ((_ error: Error) -> Void)?
+
+ init(withType type: UInt8) {
+ connectionType = type
+
+ super.init()
+ NSLog("Init yubikey connection")
+
+
+ YubiKitManager.shared.delegate = self
+ }
+
+ deinit {
+ NSLog("Deinit yubikey connection")
+ }
+
+
+ func connect(
+ completion: @escaping (_ connection: YKFConnectionProtocol) -> Void,
+ error: @escaping (_ error: Error) -> Void
+ ) {
+ self.connectionCallback = completion
+ self.connectionErrorCallback = error
+
+ if (connectionType.isEnabled(YubikeyConnectionType.Accessory)) {
+ NSLog("Attempting Accessory connection !")
+ YubiKitManager.shared.startAccessoryConnection()
+ }
+
+ if #available(iOS 13.0, *) {
+ if (connectionType.isEnabled(YubikeyConnectionType.NFC)) {
+ NSLog("Attempting NFC connection !")
+ YubiKitManager.shared.startNFCConnection()
+ }
+ }
+ }
+
+ func disconnect(successMessage successMessage: String?, errorMessage errorMessage: String?) {
+ if #available(iOS 13.0, *) {
+ if let message = errorMessage {
+ YubiKitManager.shared.stopNFCConnection(withErrorMessage: message)
+ } else if let message = successMessage {
+ YubiKitManager.shared.stopNFCConnection(withMessage: message)
+ } else {
+ YubiKitManager.shared.stopNFCConnection()
+ }
+ }
+ YubiKitManager.shared.stopAccessoryConnection()
+ }
+}
+
+extension YubiKeyConnection: YKFManagerDelegate {
+ func didConnectNFC(_ connection: YKFNFCConnection) {
+ NSLog("Did connect NFC")
+ activeConnection = connection
+ if let callback = connectionCallback {
+ NSLog("Calling callback")
+ callback(connection)
+ }
+ }
+
+ func didDisconnectNFC(_ connection: YKFNFCConnection, error: Error?) {
+ NSLog("Did disconnect NFC")
+ if #available(iOS 13.0, *) {
+ YubiKitManager.shared.stopNFCConnection(withErrorMessage: "Connection lost")
+ }
+ activeConnection = nil
+ }
+
+ func didFailConnectingNFC(_ error: Error) {
+ NSLog("Did fail to connect NFC")
+ if #available(iOS 13.0, *) {
+ YubiKitManager.shared.stopNFCConnection(withErrorMessage: error.localizedDescription)
+ }
+ activeConnection = nil
+
+ if let callback = connectionErrorCallback {
+ callback(error)
+ }
+ }
+
+ func didConnectAccessory(_ connection: YKFAccessoryConnection) {
+ NSLog("Did connect accessory")
+ activeConnection = connection
+ }
+
+ func didDisconnectAccessory(_ connection: YKFAccessoryConnection, error: Error?) {
+ NSLog("Did disconnect accessory")
+ YubiKitManager.shared.stopAccessoryConnection()
+ activeConnection = nil
+ }
+}
diff --git a/ios/Classes/Error.swift b/ios/Classes/Error.swift
new file mode 100644
index 0000000..5926219
--- /dev/null
+++ b/ios/Classes/Error.swift
@@ -0,0 +1,18 @@
+//
+// Error.swift
+// yubikit_ios
+//
+// Created by charly on 19/12/2022.
+//
+
+import Foundation
+
+enum YubikitError: String {
+ case other = "OTHER"
+ case dataError = "INVALID_DATA"
+ case alreadyConnectedFailure = "ALREADY_CONNECTED"
+ case notConnectedFailure = "NOT_CONNECTED"
+ case unsupportedOperation = "UNSUPPORTED_OPERATION"
+ case invalidPin = "INVALID_PIN"
+ case invalidMangementKey = "INVALID_MANAGEMENT_KEY"
+}
diff --git a/ios/Classes/SwiftYubikitIosPlugin.swift b/ios/Classes/SwiftYubikitIosPlugin.swift
new file mode 100644
index 0000000..7777b56
--- /dev/null
+++ b/ios/Classes/SwiftYubikitIosPlugin.swift
@@ -0,0 +1,47 @@
+import Flutter
+import UIKit
+import YubiKit
+
+
+public class SwiftYubikitIosPlugin: NSObject, FlutterPlugin {
+ public static func register(with registrar: FlutterPluginRegistrar) {
+ let channel = FlutterMethodChannel(name: "net.archethic/yubidart", binaryMessenger: registrar.messenger())
+ let instance = SwiftYubikitIosPlugin()
+ registrar.addMethodCallDelegate(instance, channel: channel)
+ }
+
+ let connection : YubiKeyConnection = YubiKeyConnection(withType: YubikeyConnectionType.NFC.rawValue | YubikeyConnectionType.Accessory.rawValue)
+
+ func failure(result: @escaping FlutterResult,
+ code:String,
+ message:String,
+ details:Any?) {
+ result(FlutterError.init(
+ code: code,
+ message: message,
+ details: details
+ ))
+ self.connection.disconnect(successMessage: nil, errorMessage: nil)
+ }
+
+ let handlers = [Handler].init(arrayLiteral:
+ PivGenerateKeyHandler(),
+ PivCalculateSecretHandler(),
+ PivGetCertificateHandler(),
+ CapabilitiesHandler()
+ )
+
+ public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ guard
+ let matchingHandler = handlers.first(where: {(handler) -> Bool in handler.canHandle(call)})
+ else {
+ result(FlutterMethodNotImplemented)
+ return
+ }
+
+ matchingHandler.handle(self, call: call, result: result)
+ }
+}
+
+
+
diff --git a/ios/Classes/YubikitIosPlugin.h b/ios/Classes/YubikitIosPlugin.h
new file mode 100644
index 0000000..fb1af21
--- /dev/null
+++ b/ios/Classes/YubikitIosPlugin.h
@@ -0,0 +1,4 @@
+#import
+
+@interface YubikitIosPlugin : NSObject
+@end
diff --git a/ios/Classes/YubikitIosPlugin.m b/ios/Classes/YubikitIosPlugin.m
new file mode 100644
index 0000000..aabd373
--- /dev/null
+++ b/ios/Classes/YubikitIosPlugin.m
@@ -0,0 +1,15 @@
+#import "YubikitIosPlugin.h"
+#if __has_include()
+#import
+#else
+// Support project import fallback if the generated compatibility header
+// is not copied when this plugin is created as a library.
+// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
+#import "yubidart-Swift.h"
+#endif
+
+@implementation YubikitIosPlugin
++ (void)registerWithRegistrar:(NSObject*)registrar {
+ [SwiftYubikitIosPlugin registerWithRegistrar:registrar];
+}
+@end
diff --git a/ios/Classes/handlers/Capabilities.swift b/ios/Classes/handlers/Capabilities.swift
new file mode 100644
index 0000000..0bfcea5
--- /dev/null
+++ b/ios/Classes/handlers/Capabilities.swift
@@ -0,0 +1,46 @@
+//
+// File.swift
+// yubidart
+//
+// Created by charly on 01/02/2023.
+//
+
+import Flutter
+import UIKit
+import YubiKit
+
+class CapabilitiesHandler: Handler {
+ private var kGetPlatformVersion = "getPlatformVersion"
+ private var kSupportsNFCScanning = "supportsNFCScanning"
+ private var kSupportsISO7816NFCTags = "supportsISO7816NFCTags"
+ private var kSupportsMFIAccessoryKey = "supportsMFIAccessoryKey"
+
+
+ func canHandle(_ call: FlutterMethodCall) -> Bool {
+ switch (call.method) {
+ case kGetPlatformVersion, kSupportsNFCScanning, kSupportsISO7816NFCTags, kSupportsMFIAccessoryKey:
+ return true
+ default:
+ return false
+ }
+ }
+
+ func handle(_ context: SwiftYubikitIosPlugin, call: FlutterMethodCall, result: @escaping FlutterResult) {
+ switch (call.method) {
+ case kGetPlatformVersion:
+ result("iOS " + UIDevice.current.systemVersion)
+ return
+ case kSupportsNFCScanning:
+ result(YubiKitDeviceCapabilities.supportsNFCScanning)
+ return
+ case kSupportsISO7816NFCTags:
+ result(YubiKitDeviceCapabilities.supportsISO7816NFCTags)
+ return
+ case kSupportsMFIAccessoryKey:
+ result(YubiKitDeviceCapabilities.supportsMFIAccessoryKey)
+ return
+ default:
+ return
+ }
+ }
+}
diff --git a/ios/Classes/handlers/Handler.swift b/ios/Classes/handlers/Handler.swift
new file mode 100644
index 0000000..0c76045
--- /dev/null
+++ b/ios/Classes/handlers/Handler.swift
@@ -0,0 +1,13 @@
+//
+// Handler.swift
+// yubidart
+//
+// Created by charly on 01/02/2023.
+//
+
+import Foundation
+
+protocol Handler {
+ func canHandle(_ call: FlutterMethodCall) -> Bool
+ func handle(_ context: SwiftYubikitIosPlugin, call: FlutterMethodCall, result: @escaping FlutterResult)
+}
diff --git a/ios/Classes/handlers/PivCalculateSecret.swift b/ios/Classes/handlers/PivCalculateSecret.swift
new file mode 100644
index 0000000..d75a256
--- /dev/null
+++ b/ios/Classes/handlers/PivCalculateSecret.swift
@@ -0,0 +1,105 @@
+//
+// File.swift
+// yubidart
+//
+// Created by charly on 01/02/2023.
+//
+
+import Flutter
+import UIKit
+import YubiKit
+
+class PivCalculateSecretHandler: Handler {
+ func canHandle(_ call: FlutterMethodCall) -> Bool {
+ call.method == "pivCalculateSecret"
+ }
+
+ func handle(_ context: SwiftYubikitIosPlugin, call: FlutterMethodCall, result: @escaping FlutterResult) {
+ var secKeyCreateError : Unmanaged?
+
+ guard
+ let args = call.arguments as? Dictionary,
+ let pin = args["pin"] as? String,
+ let rawSlot = args["slot"] as? NSNumber,
+ let slot = YKFPIVSlot(rawValue: rawSlot.uintValue),
+ let rawPeerPublicKey = args["peerPublicKey"] as? FlutterStandardTypedData,
+ let peerPublicKey = DerDecoder().decodePublicKey(rawPeerPublicKey.data as Data, &secKeyCreateError)
+ else {
+ if (secKeyCreateError != nil) {
+ result(FlutterError.init(
+ code: YubikitError.dataError.rawValue,
+ message: "Invalid Public Key",
+ details: secKeyCreateError?.takeRetainedValue().localizedDescription
+ ))
+ return
+
+ }
+ result(FlutterError.init(
+ code: YubikitError.dataError.rawValue,
+ message: "Data or format error",
+ details: call.arguments
+ ))
+ return
+ }
+
+
+ context.connection.connect(
+ completion: {(connection) -> Void in
+ connection.pivSession { session, error in
+ guard let pivSession = session else {
+ context.failure(
+ result: result,
+ code: YubikitError.other.rawValue,
+ message: "Failed to create PIV session",
+ details: error?.localizedDescription
+ )
+
+ return
+ }
+
+ pivSession.verifyPin(pin) { retries, verifyPinError in
+ guard verifyPinError == nil else {
+ context.failure(
+ result: result,
+ code: YubikitError.invalidPin.rawValue,
+ message: "Failed to verify pin",
+ details: retries
+ )
+ return
+ }
+
+ pivSession.calculateSecretKey(
+ in: slot,
+ peerPublicKey: peerPublicKey
+ ) { secretKey, error in
+ guard secretKey != nil else {
+ context.failure(
+ result: result,
+ code: YubikitError.other.rawValue,
+ message: "Failed to calculate PIV secret key",
+ details: error?.localizedDescription
+ )
+ return
+ }
+
+ result(secretKey)
+ context.connection.disconnect(successMessage: nil, errorMessage: nil)
+ }
+ }
+
+ }
+ },
+ error: {(error) -> Void in
+ context.failure(
+ result: result,
+ code: YubikitError.other.rawValue,
+ message: "Connection failed",
+ details: error.localizedDescription
+ )
+ }
+ )
+
+
+ }
+
+}
diff --git a/ios/Classes/handlers/PivGenerateKey.swift b/ios/Classes/handlers/PivGenerateKey.swift
new file mode 100644
index 0000000..e553fb1
--- /dev/null
+++ b/ios/Classes/handlers/PivGenerateKey.swift
@@ -0,0 +1,124 @@
+//
+// File.swift
+// yubidart
+//
+// Created by charly on 01/02/2023.
+//
+
+import Flutter
+import UIKit
+import YubiKit
+
+class PivGenerateKeyHandler: Handler {
+ func canHandle(_ call: FlutterMethodCall) -> Bool {
+ call.method == "pivGenerateKey"
+ }
+
+ func handle(_ context: SwiftYubikitIosPlugin, call: FlutterMethodCall, result: @escaping FlutterResult) {
+ guard let args = call.arguments as? Dictionary,
+ let pin = args["pin"] as? String,
+ let managementKey = args["managementKey"] as? FlutterStandardTypedData,
+ let rawKeyType = args["managementKeyType"] as? NSNumber,
+ let keyType = YKFPIVManagementKeyType.fromValue(rawKeyType.uint8Value),
+ let rawSlot = args["slot"] as? NSNumber,
+ let slot = YKFPIVSlot(rawValue: rawSlot.uintValue),
+ let rawType = args["type"] as? NSNumber,
+ let type = YKFPIVKeyType(rawValue: rawType.uintValue),
+ let rawPinPolicy = args["pinPolicy"] as? NSNumber,
+ let pinPolicy = YKFPIVPinPolicy(rawValue: rawPinPolicy.uintValue),
+ let rawTouchPolicy = args["touchPolicy"] as? NSNumber,
+ let touchPolicy = YKFPIVTouchPolicy(rawValue: rawTouchPolicy.uintValue)
+ else {
+ result(FlutterError.init(
+ code: YubikitError.dataError.rawValue,
+ message: "Data or format error",
+ details: call.arguments
+ ))
+ return
+ }
+
+ context.connection.connect(
+ completion: {(connection) -> Void in
+ connection.pivSession { session, error in
+ guard let pivSession = session else {
+ context.failure(
+ result: result,
+ code: YubikitError.other.rawValue,
+ message: "Failed to create PIV session",
+ details: error?.localizedDescription
+ )
+
+ return
+ }
+
+ pivSession.authenticate(
+ withManagementKey: managementKey.data,
+ type: keyType
+ ) { error in
+ guard error == nil else {
+ context.failure(
+ result: result,
+ code: YubikitError.invalidMangementKey.rawValue,
+ message: "Failed to verify management key",
+ details: error?.localizedDescription
+ )
+ return
+ }
+
+ pivSession.verifyPin(pin) { retries, verifyPinError in
+ guard verifyPinError == nil else {
+ context.failure(
+ result: result,
+ code: YubikitError.invalidPin.rawValue,
+ message: "Failed to verify pin",
+ details: retries
+ )
+ return
+ }
+
+ pivSession.generateKey(
+ in: slot,
+ type: type,
+ pinPolicy: pinPolicy,
+ touchPolicy: touchPolicy
+ ) { publicKeyRef, error in
+ guard publicKeyRef != nil else {
+ context.failure(
+ result: result,
+ code: YubikitError.other.rawValue,
+ message: "Failed to generate PIV key",
+ details: error?.localizedDescription
+ )
+ return
+ }
+
+
+
+ guard let data = SecKeyCopyExternalRepresentation(publicKeyRef!, nil) else {
+ context.failure(
+ result: result,
+ code: YubikitError.other.rawValue,
+ message: "Failed to read generated public key",
+ details: error?.localizedDescription
+ )
+ return
+ }
+
+ result(data)
+ context.connection.disconnect(successMessage: nil, errorMessage: nil)
+ }
+ }
+ }
+ }
+ },
+ error: {(error) -> Void in
+ context.failure(
+ result: result,
+ code: YubikitError.other.rawValue,
+ message: "Connection failed",
+ details: error.localizedDescription
+ )
+ }
+ )
+ }
+}
diff --git a/ios/Classes/handlers/PivGetCertificate.swift b/ios/Classes/handlers/PivGetCertificate.swift
new file mode 100644
index 0000000..d80b400
--- /dev/null
+++ b/ios/Classes/handlers/PivGetCertificate.swift
@@ -0,0 +1,93 @@
+//
+// File.swift
+// yubidart
+//
+// Created by charly on 01/02/2023.
+//
+
+import Flutter
+import UIKit
+import YubiKit
+
+class PivGetCertificateHandler: Handler {
+ func canHandle(_ call: FlutterMethodCall) -> Bool {
+ call.method == "pivGetCertificate"
+ }
+
+ func handle(_ context: SwiftYubikitIosPlugin, call: FlutterMethodCall, result: @escaping FlutterResult) {
+ var secKeyCreateError : Unmanaged?
+
+ guard
+ let args = call.arguments as? Dictionary,
+ let pin = args["pin"] as? String,
+ let rawSlot = args["slot"] as? NSNumber,
+ let slot = YKFPIVSlot(rawValue: rawSlot.uintValue)
+ else {
+ result(FlutterError.init(
+ code: YubikitError.dataError.rawValue,
+ message: "Data or format error",
+ details: call.arguments
+ ))
+ return
+ }
+
+
+ context.connection.connect(
+ completion: {(connection) -> Void in
+ connection.pivSession { session, error in
+ guard let pivSession = session else {
+ context.failure(
+ result: result,
+ code: YubikitError.other.rawValue,
+ message: "Failed to create PIV session",
+ details: error?.localizedDescription
+ )
+
+ return
+ }
+
+ pivSession.verifyPin(pin) { retries, verifyPinError in
+ guard verifyPinError == nil else {
+ context.failure(
+ result: result,
+ code: YubikitError.invalidPin.rawValue,
+ message: "Failed to verify pin",
+ details: retries
+ )
+ return
+ }
+
+ pivSession.getCertificateIn(
+ slot
+ ) { certificate, error in
+ guard certificate != nil else {
+ context.failure(
+ result: result,
+ code: YubikitError.other.rawValue,
+ message: "Failed to get certificate",
+ details: error?.localizedDescription
+ )
+ return
+ }
+
+ result(certificate)
+ context.connection.disconnect(successMessage: nil, errorMessage: nil)
+ }
+ }
+
+ }
+ },
+ error: {(error) -> Void in
+ context.failure(
+ result: result,
+ code: YubikitError.other.rawValue,
+ message: "Connection failed",
+ details: error.localizedDescription
+ )
+ }
+ )
+
+
+ }
+
+}
diff --git a/ios/Classes/utils/Data.swift b/ios/Classes/utils/Data.swift
new file mode 100644
index 0000000..016c18c
--- /dev/null
+++ b/ios/Classes/utils/Data.swift
@@ -0,0 +1,32 @@
+extension Data {
+ static func fromHexaString(_ string: String) -> Data? {
+ let length = string.count
+ if length & 1 != 0 {
+ return nil
+ }
+ var bytes = [UInt8]()
+ bytes.reserveCapacity(length/2)
+ var index = string.startIndex
+ for _ in 0.. String {
+ let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
+ return self.map { String(format: format, $0) }.joined()
+ }
+}
\ No newline at end of file
diff --git a/ios/Classes/utils/DerDecoder.swift b/ios/Classes/utils/DerDecoder.swift
new file mode 100644
index 0000000..c88de03
--- /dev/null
+++ b/ios/Classes/utils/DerDecoder.swift
@@ -0,0 +1,20 @@
+import ASN1Decoder
+
+class DerDecoder {
+ func decodePublicKey(_ data: Data, _ error: UnsafeMutablePointer?>?) -> SecKey? {
+ guard
+ let asn1 = try? ASN1DERDecoder.decode(data: data),
+ let keyData = asn1.first?.sub(1)?.value as? Data
+ else {
+ return nil
+ }
+ return SecKeyCreateWithData(
+ keyData as CFData,
+ [
+ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
+ kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
+ ] as CFDictionary,
+ error
+ )
+ }
+}
diff --git a/ios/yubidart.podspec b/ios/yubidart.podspec
new file mode 100644
index 0000000..2a3250d
--- /dev/null
+++ b/ios/yubidart.podspec
@@ -0,0 +1,26 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint yubikit_ios.podspec` to validate before publishing.
+#
+Pod::Spec.new do |s|
+ s.name = 'yubidart'
+ s.version = '0.0.1'
+ s.summary = 'A new Flutter plugin project.'
+ s.description = <<-DESC
+A new Flutter plugin project.
+ DESC
+ s.homepage = 'http://example.com'
+ s.license = { :file => '../LICENSE' }
+ s.author = { 'Your Company' => 'email@example.com' }
+ s.source = { :path => '.' }
+ s.source_files = 'Classes/**/*'
+ s.dependency 'Flutter'
+ s.platform = :ios, '9.0'
+
+ # Flutter.framework does not contain a i386 slice.
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
+ s.swift_version = '5.0'
+
+ s.dependency 'YubiKit', '~> 4.2.0'
+ s.dependency 'ASN1Decoder'
+end
diff --git a/lib/src/domain/model/failure/failure.dart b/lib/src/domain/model/failure/failure.dart
new file mode 100644
index 0000000..418e173
--- /dev/null
+++ b/lib/src/domain/model/failure/failure.dart
@@ -0,0 +1,99 @@
+import 'dart:async';
+import 'dart:developer';
+
+import 'package:flutter/services.dart';
+import 'package:yubidart/src/domain/model/failure/failure_ext.dart';
+
+abstract class YKFailure implements Exception {
+ const YKFailure();
+
+ static Future guard(FutureOr Function() run) async {
+ try {
+ return await run();
+ } on PlatformException catch (e, stack) {
+ log(
+ 'An error occured',
+ name: 'Yubidart',
+ error: e,
+ stackTrace: stack,
+ );
+ throw e.toYKFailure();
+ }
+ }
+
+ factory YKFailure.invalidPIVManagementKey({
+ String? message,
+ }) = InvalidPIVManagementKey;
+
+ factory YKFailure.securityConditionNotSatisfied() =
+ SecurityConditionNotSatisfied;
+
+ factory YKFailure.invalidPin({
+ required int remainingRetries,
+ }) = InvalidPin;
+
+ factory YKFailure.authMethodBlocked() = AuthMethodBlocked;
+
+ factory YKFailure.unsupportedOperation({
+ String? message,
+ }) = UnsupportedOperation;
+
+ factory YKFailure.deviceError() = DeviceError;
+
+ factory YKFailure.notConnected() = NotConnectedFailure;
+
+ factory YKFailure.invalidData() = InvalidData;
+
+ factory YKFailure.other() = OtherFailure;
+}
+
+class InvalidPIVManagementKey extends YKFailure {
+ final String? message;
+ const InvalidPIVManagementKey({
+ this.message,
+ });
+}
+
+class SecurityConditionNotSatisfied extends YKFailure {
+ const SecurityConditionNotSatisfied();
+}
+
+class InvalidPin extends YKFailure {
+ final int remainingRetries;
+
+ const InvalidPin({
+ required this.remainingRetries,
+ });
+}
+
+class AuthMethodBlocked extends YKFailure {
+ const AuthMethodBlocked();
+}
+
+class DeviceError extends YKFailure {
+ const DeviceError();
+}
+
+class UnsupportedOperation extends YKFailure {
+ final String? message;
+
+ const UnsupportedOperation({
+ this.message,
+ });
+}
+
+class NotConnectedFailure extends YKFailure {
+ const NotConnectedFailure();
+}
+
+class AlreadyConnectedFailure extends YKFailure {
+ const AlreadyConnectedFailure();
+}
+
+class InvalidData extends YKFailure {
+ const InvalidData();
+}
+
+class OtherFailure extends YKFailure {
+ const OtherFailure();
+}
diff --git a/lib/src/domain/model/failure/failure_ext.dart b/lib/src/domain/model/failure/failure_ext.dart
new file mode 100644
index 0000000..4a69c48
--- /dev/null
+++ b/lib/src/domain/model/failure/failure_ext.dart
@@ -0,0 +1,28 @@
+import 'package:flutter/services.dart';
+import 'package:yubidart/src/domain/model/failure/failure.dart';
+
+extension YKPlatformExceptionExt on PlatformException {
+ YKFailure toYKFailure() {
+ switch (code) {
+ case 'INVALID_DATA':
+ return const InvalidData();
+ case 'ALREADY_CONNECTED':
+ return const AlreadyConnectedFailure();
+ case 'NOT_CONNECTED':
+ return const NotConnectedFailure();
+ case 'UNSUPPORTED_OPERATION':
+ return UnsupportedOperation(message: message);
+ case 'INVALID_PIN':
+ return InvalidPin(remainingRetries: details as int);
+ case 'INVALID_MANAGEMENT_KEY':
+ return InvalidPIVManagementKey(message: message);
+ case 'AUTH_METHOD_BLOCKED':
+ return const AuthMethodBlocked();
+ case 'SECURITY_CONDITION_NOT_SATISFIED':
+ return const SecurityConditionNotSatisfied();
+ case 'DEVICE_ERROR':
+ return const DeviceError();
+ }
+ return const OtherFailure();
+ }
+}
diff --git a/lib/src/domain/model/general/device_capabilities.dart b/lib/src/domain/model/general/device_capabilities.dart
new file mode 100644
index 0000000..aede08b
--- /dev/null
+++ b/lib/src/domain/model/general/device_capabilities.dart
@@ -0,0 +1,9 @@
+class DeviceCapabilities {
+ final bool nfc;
+ final bool wired;
+
+ const DeviceCapabilities({
+ required this.nfc,
+ required this.wired,
+ });
+}
diff --git a/lib/src/domain/model/model.dart b/lib/src/domain/model/model.dart
new file mode 100644
index 0000000..592c33a
--- /dev/null
+++ b/lib/src/domain/model/model.dart
@@ -0,0 +1,12 @@
+export 'failure/failure.dart';
+export 'failure/failure_ext.dart';
+export 'general/device_capabilities.dart';
+export 'nfc/record.dart';
+export 'nfc/unsupported_record.dart';
+export 'nfc/wellknown_uri_record.dart';
+export 'piv/key_type.dart';
+export 'piv/management_key.dart';
+export 'piv/management_key_type.dart';
+export 'piv/pin_policy.dart';
+export 'piv/slot.dart';
+export 'piv/touch_policy.dart';
diff --git a/lib/src/nfc/record.dart b/lib/src/domain/model/nfc/record.dart
similarity index 76%
rename from lib/src/nfc/record.dart
rename to lib/src/domain/model/nfc/record.dart
index 47e3533..0fb11fc 100644
--- a/lib/src/nfc/record.dart
+++ b/lib/src/domain/model/nfc/record.dart
@@ -1,9 +1,8 @@
// Package imports:
import 'package:nfc_manager/nfc_manager.dart';
-
// Project imports:
-import 'package:yubidart/src/nfc/unsupported_record.dart';
-import 'package:yubidart/src/nfc/wellknown_uri_record.dart';
+import 'package:yubidart/src/domain/model/nfc/unsupported_record.dart';
+import 'package:yubidart/src/domain/model/nfc/wellknown_uri_record.dart';
// ignore: avoid_classes_with_only_static_members
abstract class Record {
diff --git a/lib/src/nfc/unsupported_record.dart b/lib/src/domain/model/nfc/unsupported_record.dart
similarity index 83%
rename from lib/src/nfc/unsupported_record.dart
rename to lib/src/domain/model/nfc/unsupported_record.dart
index c542f47..4ad0e5e 100644
--- a/lib/src/nfc/unsupported_record.dart
+++ b/lib/src/domain/model/nfc/unsupported_record.dart
@@ -1,8 +1,7 @@
// Package imports:
import 'package:nfc_manager/nfc_manager.dart';
-
// Project imports:
-import 'package:yubidart/src/nfc/record.dart';
+import 'package:yubidart/src/domain/model/nfc/record.dart';
class UnsupportedRecord implements Record {
UnsupportedRecord(this.record);
diff --git a/lib/src/nfc/wellknown_uri_record.dart b/lib/src/domain/model/nfc/wellknown_uri_record.dart
similarity index 91%
rename from lib/src/nfc/wellknown_uri_record.dart
rename to lib/src/domain/model/nfc/wellknown_uri_record.dart
index 077d827..f3ab98b 100644
--- a/lib/src/nfc/wellknown_uri_record.dart
+++ b/lib/src/domain/model/nfc/wellknown_uri_record.dart
@@ -4,9 +4,8 @@ import 'dart:typed_data';
// Package imports:
import 'package:nfc_manager/nfc_manager.dart';
-
// Project imports:
-import 'package:yubidart/src/nfc/record.dart';
+import 'package:yubidart/src/domain/model/nfc/record.dart';
class WellknownUriRecord implements Record {
WellknownUriRecord({this.identifier, required this.uri});
diff --git a/lib/src/model/verification_response.dart b/lib/src/domain/model/otp/verification_response.dart
similarity index 95%
rename from lib/src/model/verification_response.dart
rename to lib/src/domain/model/otp/verification_response.dart
index 55378d2..1d5da78 100644
--- a/lib/src/model/verification_response.dart
+++ b/lib/src/domain/model/otp/verification_response.dart
@@ -1,6 +1,6 @@
/// The verification response tells you whether the OTP is valid
/// See: https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
-class VerificationResponse {
+class OTPVerificationResponse {
/// The OTP from the YubiKey, from request
String? otp;
diff --git a/lib/src/domain/model/piv/key_type.dart b/lib/src/domain/model/piv/key_type.dart
new file mode 100644
index 0000000..c25a5ad
--- /dev/null
+++ b/lib/src/domain/model/piv/key_type.dart
@@ -0,0 +1,10 @@
+enum PivKeyType {
+ rsa1024(0x06),
+ rsa2048(0x07),
+ eccp256(0x11),
+ eccp384(0x14),
+ unknown(0x00);
+
+ const PivKeyType(this.value);
+ final int value;
+}
diff --git a/lib/src/domain/model/piv/management_key.dart b/lib/src/domain/model/piv/management_key.dart
new file mode 100644
index 0000000..a98c7f6
--- /dev/null
+++ b/lib/src/domain/model/piv/management_key.dart
@@ -0,0 +1,44 @@
+import 'dart:typed_data';
+
+import 'package:yubidart/src/domain/model/failure/failure.dart';
+import 'package:yubidart/src/domain/model/piv/management_key_type.dart';
+
+class PivManagementKey {
+ final Uint8List key;
+ final PivManagementKeyType keyType;
+
+ const PivManagementKey({
+ required this.key,
+ required this.keyType,
+ });
+
+ factory PivManagementKey.fromString(
+ String key, {
+ required PivManagementKeyType keyType,
+ }) {
+ if (key.length != 48) {
+ throw YKFailure.invalidPIVManagementKey(
+ message: 'Key should be 48 characters length',
+ );
+ }
+
+ if (key.contains(RegExp(r'[^a-fA-F0-9]'))) {
+ throw YKFailure.invalidPIVManagementKey(
+ message: 'Key should contain hexadecimal characters only',
+ );
+ }
+
+ final hexaKey = Uint8List(24);
+ for (var i = 0; i < key.length; i += 2) {
+ final digit = int.parse(
+ key.substring(i, i + 2),
+ radix: 16,
+ );
+ hexaKey[i ~/ 2] = digit;
+ }
+ return PivManagementKey(
+ key: hexaKey,
+ keyType: keyType,
+ );
+ }
+}
diff --git a/lib/src/domain/model/piv/management_key_type.dart b/lib/src/domain/model/piv/management_key_type.dart
new file mode 100644
index 0000000..6a4a800
--- /dev/null
+++ b/lib/src/domain/model/piv/management_key_type.dart
@@ -0,0 +1,9 @@
+enum PivManagementKeyType {
+ tripleDES(0x03),
+ aes128(0x08),
+ aes192(0x0a),
+ aes256(0x0c);
+
+ const PivManagementKeyType(this.value);
+ final int value;
+}
diff --git a/lib/src/domain/model/piv/pin_policy.dart b/lib/src/domain/model/piv/pin_policy.dart
new file mode 100644
index 0000000..39559b7
--- /dev/null
+++ b/lib/src/domain/model/piv/pin_policy.dart
@@ -0,0 +1,9 @@
+enum PivPinPolicy {
+ defaultPolicy(0x0),
+ never(0x1),
+ once(0x2),
+ always(0x3);
+
+ const PivPinPolicy(this.value);
+ final int value;
+}
diff --git a/lib/src/domain/model/piv/slot.dart b/lib/src/domain/model/piv/slot.dart
new file mode 100644
index 0000000..247b6bd
--- /dev/null
+++ b/lib/src/domain/model/piv/slot.dart
@@ -0,0 +1,10 @@
+enum PivSlot {
+ authentication(0x9a),
+ signature(0x9c),
+ management(0x9d),
+ cardAuth(0x9e),
+ attestation(0xf9);
+
+ const PivSlot(this.value);
+ final int value;
+}
diff --git a/lib/src/domain/model/piv/touch_policy.dart b/lib/src/domain/model/piv/touch_policy.dart
new file mode 100644
index 0000000..44166f0
--- /dev/null
+++ b/lib/src/domain/model/piv/touch_policy.dart
@@ -0,0 +1,9 @@
+enum PivTouchPolicy {
+ defaultPolicy(0x0),
+ never(0x1),
+ always(0x2),
+ cached(0x3);
+
+ const PivTouchPolicy(this.value);
+ final int value;
+}
diff --git a/lib/src/domain/protocol/general/protocol.dart b/lib/src/domain/protocol/general/protocol.dart
new file mode 100644
index 0000000..bad1961
--- /dev/null
+++ b/lib/src/domain/protocol/general/protocol.dart
@@ -0,0 +1,6 @@
+import 'package:yubidart/src/domain/model/general/device_capabilities.dart';
+
+abstract class GeneralProtocol {
+ /// Looks at the device capabilities (connectivity mainly)
+ Future get deviceCapabilities;
+}
diff --git a/lib/src/domain/protocol/otp/otp.dart b/lib/src/domain/protocol/otp/otp.dart
new file mode 100644
index 0000000..5ccc6f0
--- /dev/null
+++ b/lib/src/domain/protocol/otp/otp.dart
@@ -0,0 +1,43 @@
+import 'package:nfc_manager/nfc_manager.dart';
+import 'package:yubidart/src/domain/model/otp/verification_response.dart';
+
+abstract class OTPProtocol {
+ const OTPProtocol();
+
+ /// Get OTP from NFC YubiKey
+ /// @param {NfcTag} [tag] Tag discovered by the session
+ String getOTPFromYubiKeyNFC(NfcTag tag);
+
+ /// Verify from NFC Yubikey the OTP
+ /// @param {NfcTag} [tag] Tag discovered by the session
+ /// @param {String} [apiKey]
+ /// @param {String} [id] Specifies the requestor so that the end-point can retrieve correct shared secret for signing the response.
+ /// @param {int} [timeout] (optional) Number of seconds to wait for sync responses; if absent, let the server decide
+ /// @param {String} [sl] (optional) A value 0 to 100 indicating percentage of syncing required by client, or strings "fast" or "secure" to use server-configured values; if absent, let the server decide
+ /// @param {String} [timestamp] (optional) Timestamp=1 requests timestamp and session counter information in the response
+ Future verifyOTPFromYubiKeyNFC(
+ NfcTag tag,
+ String apiKey,
+ String id, {
+ int? timeout,
+ String? sl,
+ String? timestamp,
+ });
+
+ /// Verify OTP with YubiCloud
+ /// https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
+ /// @param {String} [otp] The OTP from the YubiKey.
+ /// @param {String} [apiKey]
+ /// @param {String} [id] Specifies the requestor so that the end-point can retrieve correct shared secret for signing the response.
+ /// @param {int} [timeout] (optional) Number of seconds to wait for sync responses; if absent, let the server decide
+ /// @param {String} [sl] (optional) A value 0 to 100 indicating percentage of syncing required by client, or strings "fast" or "secure" to use server-configured values; if absent, let the server decide
+ /// @param {String} [timestamp] (optional) Timestamp=1 requests timestamp and session counter information in the response
+ Future verify(
+ String otp,
+ String apiKey,
+ String id, {
+ int? timeout,
+ String? sl,
+ String? timestamp,
+ });
+}
diff --git a/lib/src/domain/protocol/piv/protocol.dart b/lib/src/domain/protocol/piv/protocol.dart
new file mode 100644
index 0000000..5098071
--- /dev/null
+++ b/lib/src/domain/protocol/piv/protocol.dart
@@ -0,0 +1,58 @@
+import 'package:flutter/services.dart';
+import 'package:yubidart/src/domain/model/piv/key_type.dart';
+import 'package:yubidart/src/domain/model/piv/management_key.dart';
+import 'package:yubidart/src/domain/model/piv/pin_policy.dart';
+import 'package:yubidart/src/domain/model/piv/slot.dart';
+import 'package:yubidart/src/domain/model/piv/touch_policy.dart';
+
+abstract class PivProtocol {
+ /// Generates a new key pair within the YubiKey.
+ /// This method requires authentication and pin verification.
+ ///
+ /// YubiKey FIPS does not allow RSA1024 nor PinProtocol.NEVER.
+ /// RSA key types require RSA generation, available on YubiKeys OTHER THAN 4.2.6-4.3.4.
+ /// KeyType P348 requires P384 support, available on YubiKey 4 or later.
+ /// PinPolicy or TouchPolicy other than default require support for usage policy, available on YubiKey 4 or later.
+ /// TouchPolicy.CACHED requires support for touch cached, available on YubiKey 4.3 or later.
+ /// This method is thread safe and can be invoked from any thread (main or a background thread).
+ ///
+ /// [pin] The pin. Default pin code is 123456.
+ /// [slot] The slot to generate the new key in.
+ /// [type] Which algorithm is used for key generation.
+ /// [pinPolicy] The PIN policy for using the private key.
+ /// [touchPolicy] The touch policy for using the private key.
+ ///
+ /// Throws a [YKFailure]
+ Future generateKey({
+ required String pin,
+ required PivManagementKey managementKey,
+ required PivSlot slot,
+ required PivKeyType type,
+ required PivPinPolicy pinPolicy,
+ required PivTouchPolicy touchPolicy,
+ });
+
+ /// Reads the X.509 certificate stored in the specified slot on the YubiKey.
+ ///
+ /// [pin] The pin. Default pin code is 123456.
+ /// [slot] : The slot where the certificate is stored.
+ ///
+ /// Throws a [YKFailure]
+ Future getCertificate({
+ required String pin,
+ required PivSlot slot,
+ });
+
+ /// Perform an ECDH operation with a given public key to compute a shared secret.
+ ///
+ /// [pin] The pin. Default pin code is 123456.
+ /// [slot] The slot containing the private EC key to use.
+ /// [peerPublicKey] The peer public key for the operation. This is an EllipticCurve encryption public key in PEM format.
+ ///
+ /// Throws a [YKFailure]
+ Future calculateSecret({
+ required String pin,
+ required PivSlot slot,
+ required String peerPublicKey,
+ });
+}
diff --git a/lib/src/domain/protocol/protocol.dart b/lib/src/domain/protocol/protocol.dart
new file mode 100644
index 0000000..e0ad23a
--- /dev/null
+++ b/lib/src/domain/protocol/protocol.dart
@@ -0,0 +1,3 @@
+export 'general/protocol.dart';
+export 'otp/otp.dart';
+export 'piv/protocol.dart';
diff --git a/lib/src/domain/yubidart_platform_interface.dart b/lib/src/domain/yubidart_platform_interface.dart
new file mode 100644
index 0000000..7c0527a
--- /dev/null
+++ b/lib/src/domain/yubidart_platform_interface.dart
@@ -0,0 +1,37 @@
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+import 'package:yubidart/src/domain/protocol/general/protocol.dart';
+import 'package:yubidart/src/domain/protocol/piv/protocol.dart';
+
+abstract class YubidartPlatform extends PlatformInterface {
+ /// Constructs a [YubidartPlatform].
+ YubidartPlatform() : super(token: _token);
+
+ static final Object _token = Object();
+
+ static YubidartPlatform _instance = EmptyYubidartPlatformImplementation();
+
+ /// The default instance of [YubidartPlatform] to use.
+ ///
+ /// Defaults to [MethodChannelYubidart].
+ static YubidartPlatform get instance => _instance;
+
+ PivProtocol get piv;
+
+ GeneralProtocol get general;
+
+ /// Platform-specific implementations should set this with their own
+ /// platform-specific class that extends [YubidartPlatform] when
+ /// they register themselves.
+ static set instance(YubidartPlatform instance) {
+ PlatformInterface.verifyToken(instance, _token);
+ _instance = instance;
+ }
+}
+
+class EmptyYubidartPlatformImplementation implements YubidartPlatform {
+ @override
+ GeneralProtocol get general => throw UnimplementedError();
+
+ @override
+ PivProtocol get piv => throw UnimplementedError();
+}
diff --git a/lib/src/infrastructure/protocol/general/default_general_protocol.dart b/lib/src/infrastructure/protocol/general/default_general_protocol.dart
new file mode 100644
index 0000000..e289c4e
--- /dev/null
+++ b/lib/src/infrastructure/protocol/general/default_general_protocol.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/services.dart';
+import 'package:yubidart/src/domain/model/model.dart';
+import 'package:yubidart/src/domain/protocol/general/protocol.dart';
+
+class DefaultGeneralProtocol implements GeneralProtocol {
+ /// The method channel used to interact with the native platform.
+ // @foundation.visibleForTesting
+ final methodChannel = const MethodChannel('net.archethic/yubidart');
+
+ @override
+ Future get deviceCapabilities => YKFailure.guard(
+ () async {
+ final supportsNFCScanning =
+ await methodChannel.invokeMethod('supportsNFCScanning');
+ final supportsISO7816NFCTags =
+ await methodChannel.invokeMethod('supportsISO7816NFCTags');
+ final supportsMFIAccessoryKey =
+ await methodChannel.invokeMethod('supportsMFIAccessoryKey');
+
+ if (supportsNFCScanning == null ||
+ supportsISO7816NFCTags == null ||
+ supportsMFIAccessoryKey == null) {
+ throw YKFailure.other();
+ }
+
+ return DeviceCapabilities(
+ nfc: supportsNFCScanning || supportsISO7816NFCTags,
+ wired: supportsMFIAccessoryKey,
+ );
+ },
+ );
+}
diff --git a/lib/src/infrastructure/protocol/otp/default_otp_protocol.dart b/lib/src/infrastructure/protocol/otp/default_otp_protocol.dart
new file mode 100644
index 0000000..d07060a
--- /dev/null
+++ b/lib/src/infrastructure/protocol/otp/default_otp_protocol.dart
@@ -0,0 +1,76 @@
+import 'package:nfc_manager/nfc_manager.dart';
+import 'package:yubidart/src/domain/model/nfc/record.dart';
+import 'package:yubidart/src/domain/model/nfc/wellknown_uri_record.dart';
+import 'package:yubidart/src/domain/model/otp/verification_response.dart';
+import 'package:yubidart/src/domain/protocol/otp/otp.dart';
+import 'package:yubidart/src/infrastructure/protocol/otp/yubicloud_client.dart';
+
+class DefaultOTPProtocol implements OTPProtocol {
+ final YubicloudClient yubicloudClient;
+
+ const DefaultOTPProtocol({
+ required this.yubicloudClient,
+ });
+
+ @override
+ String getOTPFromYubiKeyNFC(NfcTag tag) {
+ final Ndef? tech = Ndef.from(tag);
+ final NdefMessage? cachedMessage = tech!.cachedMessage;
+ String otp = '';
+ if (cachedMessage != null) {
+ for (int i in Iterable.generate(cachedMessage.records.length)) {
+ final NdefRecord ndefRecord = cachedMessage.records[i];
+ final record = Record.fromNdef(ndefRecord);
+ if (record is WellknownUriRecord) {
+ otp = '${record.uri}';
+ otp = otp.split('#')[1];
+ }
+ }
+ }
+ return otp;
+ }
+
+ @override
+ Future verifyOTPFromYubiKeyNFC(
+ NfcTag tag,
+ String apiKey,
+ String id, {
+ int? timeout,
+ String? sl,
+ String? timestamp,
+ }) async {
+ OTPVerificationResponse verificationResponse = OTPVerificationResponse();
+ final String otp = getOTPFromYubiKeyNFC(tag);
+ if (otp.isNotEmpty) {
+ verificationResponse = await verify(
+ otp,
+ apiKey,
+ id,
+ timeout: timeout,
+ sl: sl,
+ timestamp: timestamp,
+ );
+ } else {
+ verificationResponse.status = 'OTP_NOT_FOUND';
+ }
+ return verificationResponse;
+ }
+
+ @override
+ Future verify(
+ String otp,
+ String apiKey,
+ String id, {
+ int? timeout,
+ String? sl,
+ String? timestamp,
+ }) =>
+ yubicloudClient.verify(
+ otp: otp,
+ apiKey: apiKey,
+ id: id,
+ sl: sl,
+ timeout: timeout,
+ timestamp: timestamp,
+ );
+}
diff --git a/lib/src/infrastructure/protocol/otp/yubicloud_client.dart b/lib/src/infrastructure/protocol/otp/yubicloud_client.dart
new file mode 100644
index 0000000..f7aec72
--- /dev/null
+++ b/lib/src/infrastructure/protocol/otp/yubicloud_client.dart
@@ -0,0 +1,118 @@
+import 'dart:convert';
+import 'dart:math';
+
+import 'package:crypto/crypto.dart' as crypto;
+import 'package:http/http.dart' as http;
+import 'package:nonce/nonce.dart';
+import 'package:yubidart/src/domain/model/otp/verification_response.dart';
+
+class YubicloudClient {
+ Future verify({
+ required String otp,
+ required String apiKey,
+ required String id,
+ int? timeout,
+ String? sl,
+ String? timestamp,
+ }) async {
+ final verificationResponse = OTPVerificationResponse();
+ try {
+ final apiKeyDecode64 = base64.decode(apiKey);
+
+ /// A 16 to 40 character long string with random unique data
+ final nonce = Nonce.generate(Random().nextInt(25) + 16);
+
+ String keyValue = 'id=$id&nonce=$nonce&otp=$otp';
+ if (sl != null) {
+ keyValue = '$keyValue&sl=$sl';
+ }
+ if (timeout != null) {
+ keyValue = '$keyValue&timeout=$timeout';
+ }
+ if (timestamp != null) {
+ keyValue = '$keyValue×tamp=$timestamp';
+ }
+ final crypto.Hmac hmacSha1 = crypto.Hmac(crypto.sha1, apiKeyDecode64);
+ final crypto.Digest sha1Result = hmacSha1.convert(keyValue.codeUnits);
+
+ /// The optional HMAC-SHA1 signature for the request.
+ final hEncode64 = base64.encode(sha1Result.bytes);
+
+ final http.Response responseHttp = await http.get(
+ Uri.parse(
+ 'https://api.yubico.com/wsapi/2.0/verify?$keyValue&h=$hEncode64'),
+ );
+ bool nonceOk = false;
+ bool otpOk = false;
+ bool hOk = false;
+ String h = '';
+ if (responseHttp.statusCode == 200) {
+ final uri = Uri.parse(Uri.encodeFull(
+ 'https://api.yubico.com/wsapi/2.0/verify?${responseHttp.body.replaceAll('\n', '&').replaceAll('\r', '')}'));
+ final responseParams = List.empty(growable: true);
+ uri.queryParameters.forEach((String k, String v) {
+ if (k == 'status') {
+ verificationResponse.status = v.trim();
+ }
+ if (k == 'nonce' && v.trim() == nonce) {
+ nonceOk = true;
+ verificationResponse.nonce = v.trim();
+ }
+ if (k == 'otp' && v.trim() == otp) {
+ otpOk = true;
+ verificationResponse.otp = v.trim();
+ }
+ if (k == 'h') {
+ h = v.trim().replaceAll(' ', '+');
+ verificationResponse.h = v.trim();
+ }
+ if (k == 't') {
+ verificationResponse.t = v.trim();
+ }
+ if (k == 'timestamp') {
+ verificationResponse.timestamp = v.trim();
+ }
+ if (k == 'sessioncounter') {
+ verificationResponse.sessionCounter = v.trim();
+ }
+ if (k == 'sessionuse') {
+ verificationResponse.sessionuse = v.trim();
+ }
+ if (k == 'sl') {
+ verificationResponse.sl = int.tryParse(v.trim());
+ }
+ responseParams.add('$k=$v');
+ });
+ responseParams
+ .sort((String a, String b) => a.toString().compareTo(b.toString()));
+ bool first = true;
+ for (String element in responseParams) {
+ element.replaceAll('\r\n', '');
+ if (element.startsWith('h=') == false) {
+ if (first) {
+ keyValue = element;
+ first = false;
+ } else {
+ keyValue = '$keyValue&$element';
+ }
+ }
+ }
+
+ if (verificationResponse.status == 'OK') {
+ final crypto.Digest responseSha1Result =
+ hmacSha1.convert(keyValue.codeUnits);
+ final responseHEncode64 = base64.encode(responseSha1Result.bytes);
+ if (responseHEncode64 == h) {
+ hOk = true;
+ }
+ if (!nonceOk || !otpOk || !hOk) {
+ verificationResponse.status = 'RESPONSE_KO';
+ }
+ }
+ }
+ } catch (e) {
+ verificationResponse.status = 'RESPONSE_KO';
+ }
+ return verificationResponse;
+ }
+}
diff --git a/lib/src/infrastructure/protocol/piv/default_piv_protocol.dart b/lib/src/infrastructure/protocol/piv/default_piv_protocol.dart
new file mode 100644
index 0000000..57c8698
--- /dev/null
+++ b/lib/src/infrastructure/protocol/piv/default_piv_protocol.dart
@@ -0,0 +1,90 @@
+import 'dart:convert';
+import 'dart:developer';
+
+// ignore: depend_on_referenced_packages
+import 'package:flutter/services.dart';
+import 'package:pem/pem.dart';
+import 'package:yubidart/src/domain/model/model.dart';
+import 'package:yubidart/src/domain/protocol/piv/protocol.dart';
+
+class DefaultPivProtocol implements PivProtocol {
+ /// The method channel used to interact with the native platform.
+ // @foundation.visibleForTesting
+ final methodChannel = const MethodChannel('net.archethic/yubidart');
+
+ @override
+ Future generateKey({
+ required String pin,
+ required PivManagementKey managementKey,
+ required PivSlot slot,
+ required PivKeyType type,
+ required PivPinPolicy pinPolicy,
+ required PivTouchPolicy touchPolicy,
+ }) =>
+ YKFailure.guard(
+ () async {
+ final result = await methodChannel.invokeMethod(
+ 'pivGenerateKey',
+ {
+ 'pin': pin,
+ 'managementKey': managementKey.key,
+ 'managementKeyType': managementKey.keyType.value,
+ 'slot': slot.value,
+ 'type': type.value,
+ 'pinPolicy': pinPolicy.value,
+ 'touchPolicy': touchPolicy.value,
+ },
+ );
+ log('result : ${json.encode(result)}');
+ if (result == null) {
+ throw YKFailure.other();
+ }
+ return result;
+ },
+ );
+
+ @override
+ Future getCertificate({
+ required String pin,
+ required PivSlot slot,
+ }) =>
+ YKFailure.guard(
+ () async {
+ final result = await methodChannel.invokeMethod(
+ 'pivGetCertificate',
+ {
+ 'pin': pin,
+ 'slot': slot.value,
+ },
+ );
+ log('result : ${json.encode(result)}');
+ if (result == null) {
+ throw YKFailure.other();
+ }
+ return result;
+ },
+ );
+
+ @override
+ Future calculateSecret({
+ required PivSlot slot,
+ required String pin,
+ required String peerPublicKey,
+ }) async {
+ final result = await methodChannel.invokeMethod(
+ 'pivCalculateSecret',
+ {
+ 'slot': slot.value,
+ 'pin': pin,
+ 'peerPublicKey': Uint8List.fromList(
+ PemCodec(PemLabel.publicKey).decode(peerPublicKey),
+ ),
+ },
+ );
+ log('result : ${json.encode(result)}');
+ if (result == null) {
+ throw YKFailure.other();
+ }
+ return result;
+ }
+}
diff --git a/lib/src/infrastructure/yubidart_android.dart b/lib/src/infrastructure/yubidart_android.dart
new file mode 100644
index 0000000..ce9ba0b
--- /dev/null
+++ b/lib/src/infrastructure/yubidart_android.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:yubidart/src/domain/model/general/device_capabilities.dart';
+import 'package:yubidart/src/domain/protocol/general/protocol.dart';
+import 'package:yubidart/src/domain/protocol/piv/protocol.dart';
+import 'package:yubidart/src/domain/yubidart_platform_interface.dart';
+import 'package:yubidart/src/infrastructure/protocol/piv/default_piv_protocol.dart';
+
+/// An implementation of [YubidartPlatform] for Android.
+class YubidartAndroid extends YubidartPlatform {
+ /// The method channel used to interact with the native platform.
+ @visibleForTesting
+ final methodChannel = const MethodChannel('net.archethic/yubidart');
+
+ static void registerWith() {
+ YubidartPlatform.instance = YubidartAndroid();
+ }
+
+ @override
+ GeneralProtocol get general => DumbGeneralProtocol();
+
+ @override
+ PivProtocol get piv => DefaultPivProtocol();
+}
+
+class DumbGeneralProtocol implements GeneralProtocol {
+ @override
+ Future get deviceCapabilities async =>
+ const DeviceCapabilities(
+ nfc: true,
+ wired: true,
+ );
+}
diff --git a/lib/src/infrastructure/yubidart_ios.dart b/lib/src/infrastructure/yubidart_ios.dart
new file mode 100644
index 0000000..b068cc7
--- /dev/null
+++ b/lib/src/infrastructure/yubidart_ios.dart
@@ -0,0 +1,18 @@
+import 'package:yubidart/src/domain/protocol/general/protocol.dart';
+import 'package:yubidart/src/domain/protocol/piv/protocol.dart';
+import 'package:yubidart/src/domain/yubidart_platform_interface.dart';
+import 'package:yubidart/src/infrastructure/protocol/general/default_general_protocol.dart';
+import 'package:yubidart/src/infrastructure/protocol/piv/default_piv_protocol.dart';
+
+/// An implementation of [YubidartPlatform] that uses method channels.
+class YubidartIos extends YubidartPlatform {
+ static void registerWith() {
+ YubidartPlatform.instance = YubidartIos();
+ }
+
+ @override
+ GeneralProtocol get general => DefaultGeneralProtocol();
+
+ @override
+ PivProtocol get piv => DefaultPivProtocol();
+}
diff --git a/lib/src/services/yubico_service.dart b/lib/src/services/yubico_service.dart
deleted file mode 100644
index 0766b8b..0000000
--- a/lib/src/services/yubico_service.dart
+++ /dev/null
@@ -1,174 +0,0 @@
-// Dart imports:
-import 'dart:async';
-import 'dart:convert';
-import 'dart:math';
-import 'dart:typed_data';
-
-// Package imports:
-import 'package:crypto/crypto.dart' as crypto show Hmac, sha1, Digest;
-import 'package:http/http.dart' as http show Response, get;
-import 'package:nfc_manager/nfc_manager.dart';
-import 'package:nonce/nonce.dart';
-
-// Project imports:
-import 'package:yubidart/src/model/verification_response.dart';
-import 'package:yubidart/src/nfc/record.dart';
-import 'package:yubidart/src/nfc/wellknown_uri_record.dart';
-
-class YubicoService {
- /// Verify OTP with YubiCloud
- /// https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
- /// @param {String} [otp] The OTP from the YubiKey.
- /// @param {String} [apiKey]
- /// @param {String} [id] Specifies the requestor so that the end-point can retrieve correct shared secret for signing the response.
- /// @param {int} [timeout] (optional) Number of seconds to wait for sync responses; if absent, let the server decide
- /// @param {String} [sl] (optional) A value 0 to 100 indicating percentage of syncing required by client, or strings "fast" or "secure" to use server-configured values; if absent, let the server decide
- /// @param {String} [timestamp] (optional) Timestamp=1 requests timestamp and session counter information in the response
- Future verifyYubiCloudOTP(
- String otp, String apiKey, String id,
- {int? timeout, String? sl, String? timestamp}) async {
- // ignore: prefer_final_locals
- VerificationResponse verificationResponse = VerificationResponse();
- try {
- final Uint8List apiKeyDecode64 = base64.decode(apiKey);
-
- /// A 16 to 40 character long string with random unique data
- final String nonce = Nonce.generate(Random().nextInt(25) + 16);
-
- String keyValue = 'id=$id&nonce=$nonce&otp=$otp';
- if (sl != null) {
- keyValue = '$keyValue&sl=$sl';
- }
- if (timeout != null) {
- keyValue = '$keyValue&timeout=$timeout';
- }
- if (timestamp != null) {
- keyValue = '$keyValue×tamp=$timestamp';
- }
- final crypto.Hmac hmacSha1 = crypto.Hmac(crypto.sha1, apiKeyDecode64);
- final crypto.Digest sha1Result = hmacSha1.convert(keyValue.codeUnits);
-
- /// The optional HMAC-SHA1 signature for the request.
- final String hEncode64 = base64.encode(sha1Result.bytes);
-
- final http.Response responseHttp = await http.get(
- Uri.parse(
- 'https://api.yubico.com/wsapi/2.0/verify?$keyValue&h=$hEncode64'),
- );
- bool nonceOk = false;
- bool otpOk = false;
- bool hOk = false;
- String h = '';
- if (responseHttp.statusCode == 200) {
- final Uri uri = Uri.parse(Uri.encodeFull(
- 'https://api.yubico.com/wsapi/2.0/verify?${responseHttp.body.replaceAll('\n', '&').replaceAll('\r', '')}'));
- // ignore: prefer_final_locals
- List responseParams = List.empty(growable: true);
- uri.queryParameters.forEach((String k, String v) {
- if (k == 'status') {
- verificationResponse.status = v.trim();
- }
- if (k == 'nonce' && v.trim() == nonce) {
- nonceOk = true;
- verificationResponse.nonce = v.trim();
- }
- if (k == 'otp' && v.trim() == otp) {
- otpOk = true;
- verificationResponse.otp = v.trim();
- }
- if (k == 'h') {
- h = v.trim().replaceAll(' ', '+');
- verificationResponse.h = v.trim();
- }
- if (k == 't') {
- verificationResponse.t = v.trim();
- }
- if (k == 'timestamp') {
- verificationResponse.timestamp = v.trim();
- }
- if (k == 'sessioncounter') {
- verificationResponse.sessionCounter = v.trim();
- }
- if (k == 'sessionuse') {
- verificationResponse.sessionuse = v.trim();
- }
- if (k == 'sl') {
- verificationResponse.sl = int.tryParse(v.trim());
- }
- responseParams.add('$k=$v');
- });
- responseParams
- .sort((String a, String b) => a.toString().compareTo(b.toString()));
- bool first = true;
- for (String element in responseParams) {
- element.replaceAll('\r\n', '');
- if (element.startsWith('h=') == false) {
- if (first) {
- keyValue = element;
- first = false;
- } else {
- keyValue = '$keyValue&$element';
- }
- }
- }
-
- if (verificationResponse.status == 'OK') {
- final crypto.Digest responseSha1Result =
- hmacSha1.convert(keyValue.codeUnits);
- final String responseHEncode64 =
- base64.encode(responseSha1Result.bytes);
- if (responseHEncode64 == h) {
- hOk = true;
- }
- if (!nonceOk || !otpOk || !hOk) {
- verificationResponse.status = 'RESPONSE_KO';
- }
- }
- }
- } catch (e) {
- print(e);
- verificationResponse.status = 'RESPONSE_KO';
- }
- return verificationResponse;
- }
-
- /// Get OTP from NFC YubiKey
- /// @param {NfcTag} [tag] Tag discovered by the session
- String getOTPFromYubiKeyNFC(NfcTag tag) {
- final Ndef? tech = Ndef.from(tag);
- final NdefMessage? cachedMessage = tech!.cachedMessage;
- String otp = '';
- if (cachedMessage != null) {
- for (int i in Iterable.generate(cachedMessage.records.length)) {
- final NdefRecord record = cachedMessage.records[i];
- final Record _record = Record.fromNdef(record);
- if (_record is WellknownUriRecord) {
- otp = '${_record.uri}';
- otp = otp.split('#')[1];
- }
- }
- }
- return otp;
- }
-
- /// Verify from NFC Yubikey the OTP
- /// @param {NfcTag} [tag] Tag discovered by the session
- /// @param {String} [apiKey]
- /// @param {String} [id] Specifies the requestor so that the end-point can retrieve correct shared secret for signing the response.
- /// @param {int} [timeout] (optional) Number of seconds to wait for sync responses; if absent, let the server decide
- /// @param {String} [sl] (optional) A value 0 to 100 indicating percentage of syncing required by client, or strings "fast" or "secure" to use server-configured values; if absent, let the server decide
- /// @param {String} [timestamp] (optional) Timestamp=1 requests timestamp and session counter information in the response
- Future verifyOTPFromYubiKeyNFC(
- NfcTag tag, String apiKey, String id,
- {int? timeout, String? sl, String? timestamp}) async {
- VerificationResponse verificationResponse = VerificationResponse();
- final String otp = getOTPFromYubiKeyNFC(tag);
- if (otp.isNotEmpty) {
- verificationResponse = await verifyYubiCloudOTP(otp, apiKey, id,
- timeout: timeout, sl: sl, timestamp: timestamp);
- } else {
- verificationResponse.status = 'OTP_NOT_FOUND';
- }
- return verificationResponse;
- }
-}
diff --git a/lib/yubidart.dart b/lib/yubidart.dart
index 3741c7d..94c86cb 100644
--- a/lib/yubidart.dart
+++ b/lib/yubidart.dart
@@ -1,5 +1,20 @@
-/// Package yubidart aims to provide a easy way to use Yubico services with Yubikey.
-library yubidart;
+import 'package:yubidart/src/domain/protocol/protocol.dart';
+import 'package:yubidart/src/domain/yubidart_platform_interface.dart';
-export 'src/model/verification_response.dart';
-export 'src/services/yubico_service.dart';
+import 'src/infrastructure/protocol/otp/default_otp_protocol.dart';
+import 'src/infrastructure/protocol/otp/yubicloud_client.dart';
+
+export 'package:cryptography/dart.dart';
+
+export 'src/domain/model/model.dart';
+export 'src/domain/protocol/protocol.dart';
+export 'src/infrastructure/yubidart_android.dart';
+export 'src/infrastructure/yubidart_ios.dart';
+
+class Yubidart {
+ GeneralProtocol get general => YubidartPlatform.instance.general;
+
+ OTPProtocol get otp => DefaultOTPProtocol(yubicloudClient: YubicloudClient());
+
+ PivProtocol get piv => YubidartPlatform.instance.piv;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index a3b8c66..91499ba 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,16 +1,20 @@
name: yubidart
-description: Yubico Services for Dart and Flutter. OTP Validation with Yubikey
+description: Yubico Services for Dart and Flutter.
homepage: https://github.com/reddwarf03/yubidart
-version: 1.0.4
+version: 2.0.0-dev.1
environment:
- sdk: '>=2.17.0 <3.0.0'
+ sdk: '>=2.18.4 <3.0.0'
dependencies:
# Implementations of SHA, MD5, and HMAC cryptographic functions
crypto: ^3.0.2
+ cryptography: ^2.0.5
+
+ flutter:
+ sdk: flutter
# A composable, multi-platform, Future-based API for HTTP requests
http: ^0.13.5
@@ -21,8 +25,26 @@ dependencies:
# Nonce contains a static generator that generates random alphanumeric strings, and can be constructed as an object containing a generated string
nonce: ^1.2.0
+ pem: ^2.0.1
+
+ plugin_platform_interface: ^2.0.2
+ # pointycastle: ^3.6.2
+
dev_dependencies:
flutter_lints: ^2.0.1
+ flutter_test:
+ sdk: flutter
# Automatically organize your dart imports
import_sorter: ^4.6.0
- test: ^1.21.4
\ No newline at end of file
+ test: ^1.21.4
+
+flutter:
+ plugin:
+ platforms:
+ ios:
+ pluginClass: YubikitIosPlugin
+ dartPluginClass: YubidartIos
+ android:
+ package: net.archethic.yubikit_android
+ pluginClass: YubikitAndroidPlugin
+ dartPluginClass: YubidartAndroid
diff --git a/test/piv_management_key_test.dart b/test/piv_management_key_test.dart
new file mode 100644
index 0000000..cea97e4
--- /dev/null
+++ b/test/piv_management_key_test.dart
@@ -0,0 +1,96 @@
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:yubidart/src/domain/model/failure/failure.dart';
+import 'package:yubidart/src/domain/model/piv/management_key.dart';
+import 'package:yubidart/src/domain/model/piv/management_key_type.dart';
+
+void main() {
+ group('PIV Management key', () {
+ group('Build from String', () {
+ test(
+ 'Should succeed with valid key',
+ () async {
+ final managementKey = PivManagementKey.fromString(
+ '000102030405060708090A0B0C0D0E0F1011121314151617',
+ keyType: PivManagementKeyType.aes128,
+ );
+
+ expect(
+ managementKey.key,
+ Uint8List.fromList([
+ 0,
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 15,
+ 16,
+ 17,
+ 18,
+ 19,
+ 20,
+ 21,
+ 22,
+ 23,
+ ]),
+ );
+
+ expect(
+ managementKey.keyType,
+ PivManagementKeyType.aes128,
+ );
+ },
+ );
+
+ test(
+ 'Should reject when length != 48 characters',
+ () async {
+ expect(
+ () => PivManagementKey.fromString(
+ '0123456',
+ keyType: PivManagementKeyType.aes128,
+ ),
+ throwsA(
+ predicate(
+ (Object? e) =>
+ e is InvalidPIVManagementKey &&
+ e.message == 'Key should be 48 characters length',
+ ),
+ ),
+ );
+ },
+ );
+
+ test(
+ 'Should reject non-hexadecimal characters',
+ () async {
+ expect(
+ () => PivManagementKey.fromString(
+ '00000000000000000000000000000000000000000000000v',
+ keyType: PivManagementKeyType.aes128,
+ ),
+ throwsA(
+ predicate(
+ (Object? e) =>
+ e is InvalidPIVManagementKey &&
+ e.message ==
+ 'Key should contain hexadecimal characters only',
+ ),
+ ),
+ );
+ },
+ );
+ });
+ });
+}
diff --git a/test/ykfailure_test.dart b/test/ykfailure_test.dart
new file mode 100644
index 0000000..a244f53
--- /dev/null
+++ b/test/ykfailure_test.dart
@@ -0,0 +1,129 @@
+// ignore: depend_on_referenced_packages
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:yubidart/src/domain/model/failure/failure.dart';
+
+Future _shouldTransformPlatforException({
+ required PlatformException platformException,
+ required Matcher exceptionMatcher,
+}) async {
+ await expectLater(
+ () => YKFailure.guard(
+ () => throw platformException,
+ ),
+ throwsA(exceptionMatcher),
+ );
+}
+
+void main() {
+ group('YKFailure', () {
+ group('Guard PlatformException', () {
+ test(
+ 'Should transform code INVALID_DATA to InvalidData',
+ () async {
+ await _shouldTransformPlatforException(
+ platformException: PlatformException(code: 'INVALID_DATA'),
+ exceptionMatcher: isA(),
+ );
+ },
+ );
+
+ test(
+ 'Should transform code ALREADY_CONNECTED to AlreadyConnectedFailure',
+ () async {
+ await _shouldTransformPlatforException(
+ platformException: PlatformException(code: 'ALREADY_CONNECTED'),
+ exceptionMatcher: isA(),
+ );
+ },
+ );
+
+ test(
+ 'Should transform code NOT_CONNECTED to NotConnectedFailure',
+ () async {
+ await _shouldTransformPlatforException(
+ platformException: PlatformException(code: 'NOT_CONNECTED'),
+ exceptionMatcher: isA(),
+ );
+ },
+ );
+
+ test(
+ 'Should transform code UNSUPPORTED_OPERATION to UnsupportedOperation',
+ () async {
+ await _shouldTransformPlatforException(
+ platformException: PlatformException(
+ code: 'UNSUPPORTED_OPERATION',
+ message: 'error description',
+ ),
+ exceptionMatcher: predicate(
+ (e) =>
+ e is UnsupportedOperation && e.message == 'error description',
+ ),
+ );
+ },
+ );
+
+ test(
+ 'Should transform code INVALID_PIN to InvalidPin',
+ () async {
+ await _shouldTransformPlatforException(
+ platformException: PlatformException(
+ code: 'INVALID_PIN',
+ details: 3,
+ ),
+ exceptionMatcher: predicate(
+ (e) => e is InvalidPin && e.remainingRetries == 3,
+ ),
+ );
+ },
+ );
+
+ test(
+ 'Should transform code INVALID_MANAGEMENT_KEY to InvalidPIVManagementKey',
+ () async {
+ await _shouldTransformPlatforException(
+ platformException: PlatformException(
+ code: 'INVALID_MANAGEMENT_KEY',
+ ),
+ exceptionMatcher: isA(),
+ );
+ },
+ );
+ test(
+ 'Should transform code AUTH_METHOD_BLOCKED to AuthMethodBlocked',
+ () async {
+ await _shouldTransformPlatforException(
+ platformException: PlatformException(
+ code: 'AUTH_METHOD_BLOCKED',
+ ),
+ exceptionMatcher: isA(),
+ );
+ },
+ );
+
+ test(
+ 'Should transform code SECURITY_CONDITION_NOT_SATISFIED to SecurityConditionNotSatisfied',
+ () async {
+ await _shouldTransformPlatforException(
+ platformException: PlatformException(
+ code: 'SECURITY_CONDITION_NOT_SATISFIED',
+ ),
+ exceptionMatcher: isA(),
+ );
+ },
+ );
+ test(
+ 'Should transform code DEVICE_ERROR to DeviceError',
+ () async {
+ await _shouldTransformPlatforException(
+ platformException: PlatformException(
+ code: 'DEVICE_ERROR',
+ ),
+ exceptionMatcher: isA(),
+ );
+ },
+ );
+ });
+ });
+}
diff --git a/test/yubico_test.dart b/test/yubico_test.dart
index 8b21244..368c39b 100644
--- a/test/yubico_test.dart
+++ b/test/yubico_test.dart
@@ -1,23 +1,21 @@
-library test.yubico_test;
-
-// Package imports:
-import 'package:test/test.dart';
-
-// Project imports:
-import 'package:yubidart/src/model/verification_response.dart';
-import 'package:yubidart/src/services/yubico_service.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:yubidart/src/domain/model/otp/verification_response.dart';
+import 'package:yubidart/src/infrastructure/protocol/otp/yubicloud_client.dart';
void main() {
- group('yubicoService', () {
- test('verifySignatures', () async {
- final VerificationResponse verificationResponse = await YubicoService()
- .verifyYubiCloudOTP('vvbbbbcggtlihvuckbitgibhcdvtblnkrvrkbhidifjn',
- 'mG5be6ZJU1qBGz24yPh/ESM3UdU=', '1');
- expect(verificationResponse.status, 'OK');
- }, tags: ['noCI']);
-
- test('ciOk', () {
- expect(true, true);
- });
+ group('YubicloudClient', () {
+ test(
+ 'verifySignatures',
+ () async {
+ final OTPVerificationResponse verificationResponse =
+ await YubicloudClient().verify(
+ otp: 'vvbbbbcggtlihvuckbitgibhcdvtblnkrvrkbhidifjn',
+ apiKey: 'mG5be6ZJU1qBGz24yPh/ESM3UdU=',
+ id: '1',
+ );
+ expect(verificationResponse.status, 'OK');
+ },
+ tags: ['noCI'],
+ );
});
}