From fa861b549658cdacc2e69bde21c45d68b0541ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awomir=20Kro=CC=81l?= Date: Wed, 10 Dec 2025 11:05:32 +0100 Subject: [PATCH 1/2] Add SPM support, add base64 challenge support, implement generateAssertion on iOS --- example/.gitignore | 2 + .../plugin_integration_test.dart | 39 ++++- example/ios/Flutter/AppFrameworkInfo.plist | 40 ++--- example/ios/Flutter/Debug.xcconfig | 1 - example/ios/Flutter/Release.xcconfig | 1 - example/ios/Podfile | 44 ----- example/ios/Podfile.lock | 28 ---- example/ios/Runner.xcodeproj/project.pbxproj | 132 ++++----------- .../xcshareddata/xcschemes/Runner.xcscheme | 18 +++ .../contents.xcworkspacedata | 3 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - example/ios/Runner/AppDelegate.swift | 2 +- example/lib/main.dart | 111 ++++++++++--- example/pubspec.lock | 64 ++++---- example/pubspec.yaml | 63 +------- ios/.gitignore | 2 + ios/Assets/.gitkeep | 0 ios/Classes/AppDeviceIntegrity.swift | 78 --------- ios/app_device_integrity.podspec | 7 +- ios/app_device_integrity/Package.swift | 25 +++ .../AppDeviceIntegrity.swift | 153 ++++++++++++++++++ .../AppDeviceIntegrityPlugin.swift | 31 +++- .../PrivacyInfo.xcprivacy | 22 +++ lib/app_device_integrity.dart | 51 +++++- lib/app_device_integrity_method_channel.dart | 28 +++- ...p_device_integrity_platform_interface.dart | 15 +- pubspec.yaml | 6 +- ..._device_integrity_method_channel_test.dart | 21 ++- test/app_device_integrity_test.dart | 40 ++++- 30 files changed, 584 insertions(+), 459 deletions(-) delete mode 100644 example/ios/Podfile delete mode 100644 example/ios/Podfile.lock delete mode 100644 ios/Assets/.gitkeep delete mode 100644 ios/Classes/AppDeviceIntegrity.swift create mode 100644 ios/app_device_integrity/Package.swift create mode 100644 ios/app_device_integrity/Sources/app_device_integrity/AppDeviceIntegrity.swift rename ios/{Classes => app_device_integrity/Sources/app_device_integrity}/AppDeviceIntegrityPlugin.swift (65%) create mode 100644 ios/app_device_integrity/Sources/app_device_integrity/PrivacyInfo.xcprivacy diff --git a/example/.gitignore b/example/.gitignore index 29a3a50..79c113f 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/example/integration_test/plugin_integration_test.dart b/example/integration_test/plugin_integration_test.dart index 5fe8144..a27863c 100644 --- a/example/integration_test/plugin_integration_test.dart +++ b/example/integration_test/plugin_integration_test.dart @@ -6,19 +6,42 @@ // For more information about Flutter integration tests, please see // https://docs.flutter.dev/cookbook/testing/integration/introduction +import 'package:app_device_integrity/app_device_integrity.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:app_device_integrity/app_device_integrity.dart'; - void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('getPlatformVersion test', (WidgetTester tester) async { - final AppDeviceIntegrity plugin = AppDeviceIntegrity(); - final String? version = await plugin.getPlatformVersion(); - // The version string depends on the host platform running the test, so - // just assert that some non-empty string is returned. - expect(version?.isNotEmpty, true); + testWidgets('getAttestationServiceSupport test', (WidgetTester tester) async { + const plugin = AppDeviceIntegrity(); + final challengeString = + 'test_challenge_${DateTime.now().millisecondsSinceEpoch}'; + + try { + final attestationToken = await plugin.getAttestationServiceSupport( + challengeString: challengeString, + ); + + // If we get here without exception, attestation is supported + // Verify the token is returned + expect(attestationToken, isNotNull); + expect(attestationToken, isA()); + debugPrint('Attestation successful: $attestationToken'); + } on PlatformException catch (error) { + // On iOS simulator and some devices, App Attest is not available + // Error code -4 means "Failed to initialize AppDeviceIntegrity" + if (error.code == '-4') { + debugPrint( + 'App Attest not supported on this device/simulator (expected)'); + // This is expected on simulator, test passes + expect(error.message, contains('Failed to initialize')); + } else { + // Other errors should fail the test + rethrow; + } + } }); } diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c56964..05a7653 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -2,25 +2,25 @@ - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 16.0 diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index ec97fc6..592ceee 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1 @@ -#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 index c4855bf..592ceee 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1,2 +1 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile deleted file mode 100644 index d97f17e..0000000 --- a/example/ios/Podfile +++ /dev/null @@ -1,44 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '12.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__)) - target 'RunnerTests' do - inherit! :search_paths - end -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 deleted file mode 100644 index 2f8bca5..0000000 --- a/example/ios/Podfile.lock +++ /dev/null @@ -1,28 +0,0 @@ -PODS: - - app_device_integrity (0.0.1): - - Flutter - - Flutter (1.0.0) - - integration_test (0.0.1): - - Flutter - -DEPENDENCIES: - - app_device_integrity (from `.symlinks/plugins/app_device_integrity/ios`) - - Flutter (from `Flutter`) - - integration_test (from `.symlinks/plugins/integration_test/ios`) - -EXTERNAL SOURCES: - app_device_integrity: - :path: ".symlinks/plugins/app_device_integrity/ios" - Flutter: - :path: Flutter - integration_test: - :path: ".symlinks/plugins/integration_test/ios" - -SPEC CHECKSUMS: - app_device_integrity: 9be0287fa5c96c408b25a6163c7a5138777a9cf6 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - integration_test: 13825b8a9334a850581300559b8839134b124670 - -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 - -COCOAPODS: 1.13.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 22b2e46..68b8a60 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,11 +11,10 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 808E72CE77CB2BFEA49CCFEC /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE9B2F860A81F6F7E24487C8 /* Pods_RunnerTests.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 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 */; }; - B1DC21C78795661A0C38EDC1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9FFB8029D0A4982FB359104 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,8 +49,6 @@ 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 = ""; }; - 7FB8A37BE0046E056B463539 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 8FF6560D2DEAB82B0C967173 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.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; }; @@ -59,12 +56,6 @@ 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 = ""; }; - A20FA94908D8DDFF1AAD9A77 /* 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 = ""; }; - B9FFB8029D0A4982FB359104 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - BE9B2F860A81F6F7E24487C8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C917FB3340B8125D6148583A /* 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 = ""; }; - E0B5B3253DF6F91B7C5EC8A1 /* 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 = ""; }; - FF933C7A8841FEB5E8603147 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,7 +63,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 808E72CE77CB2BFEA49CCFEC /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -80,7 +70,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B1DC21C78795661A0C38EDC1 /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -113,8 +103,6 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - C21B41AF3433E6DA864968B2 /* Pods */, - BEA121C6C3DF084CAE93E181 /* Frameworks */, ); sourceTree = ""; }; @@ -142,29 +130,6 @@ path = Runner; sourceTree = ""; }; - BEA121C6C3DF084CAE93E181 /* Frameworks */ = { - isa = PBXGroup; - children = ( - B9FFB8029D0A4982FB359104 /* Pods_Runner.framework */, - BE9B2F860A81F6F7E24487C8 /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - C21B41AF3433E6DA864968B2 /* Pods */ = { - isa = PBXGroup; - children = ( - C917FB3340B8125D6148583A /* Pods-Runner.debug.xcconfig */, - E0B5B3253DF6F91B7C5EC8A1 /* Pods-Runner.release.xcconfig */, - A20FA94908D8DDFF1AAD9A77 /* Pods-Runner.profile.xcconfig */, - 7FB8A37BE0046E056B463539 /* Pods-RunnerTests.debug.xcconfig */, - 8FF6560D2DEAB82B0C967173 /* Pods-RunnerTests.release.xcconfig */, - FF933C7A8841FEB5E8603147 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -172,7 +137,6 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 3E8428555E2805C61B739C1A /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 5E2286B25175904FAE3AAE60 /* Frameworks */, @@ -191,20 +155,21 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 26FBAEA836D78A37EEFB3B9D /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 714F10B1122541EB7FBA6041 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -238,6 +203,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -270,28 +238,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 26FBAEA836D78A37EEFB3B9D /* [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; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -308,45 +254,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 3E8428555E2805C61B739C1A /* [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-RunnerTests-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; - }; - 714F10B1122541EB7FBA6041 /* [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; - }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -472,6 +379,7 @@ DEVELOPMENT_TEAM = T8SJ2U9YFD; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -486,12 +394,12 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7FB8A37BE0046E056B463539 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = co.bubotech.appDeviceIntegrityExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -504,12 +412,12 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8FF6560D2DEAB82B0C967173 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = co.bubotech.appDeviceIntegrityExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -520,12 +428,12 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FF933C7A8841FEB5E8603147 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = co.bubotech.appDeviceIntegrityExample.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -651,6 +559,7 @@ DEVELOPMENT_TEAM = T8SJ2U9YFD; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -674,6 +583,7 @@ DEVELOPMENT_TEAM = T8SJ2U9YFD; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -720,6 +630,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8e3ca5d..15c313e 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + - - diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 18d9810..e69de29 100644 --- a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings index f9b0d7c..e69de29 100644 --- a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4..b636303 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/example/lib/main.dart b/example/lib/main.dart index ac8767d..28fb72b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,9 +1,11 @@ +import 'dart:async'; +import 'dart:convert'; import 'dart:io'; + +import 'package:app_device_integrity/app_device_integrity.dart'; import 'package:app_device_integrity_example/services/providers/attest_provider.dart'; import 'package:flutter/material.dart'; -import 'dart:async'; import 'package:flutter/services.dart'; -import 'package:app_device_integrity/app_device_integrity.dart'; void main() { runApp(const MyApp()); @@ -18,7 +20,9 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { String _tokenExample = 'UNKNOWN'; - final _appAttestationPlugin = AppDeviceIntegrity(); + String _keyID = ''; + String _assertionExample = 'UNKNOWN'; + final _appAttestationPlugin = const AppDeviceIntegrity(); @override void initState() { @@ -27,27 +31,66 @@ class _MyAppState extends State { } Future initPlatformState() async { - var tokenReceived; AttestProvider attestProvider = AttestProvider(); try { String sessionId = await attestProvider.getSession(); - if (Platform.isAndroid) { - int gpc = 0000000000; // YOUR GCP PROJECT ID IN ANDROID - tokenReceived = await _appAttestationPlugin - .getAttestationServiceSupport(challengeString: sessionId, gcp: gpc); + + int? gpc = Platform.isAndroid + ? 0000000000 + : null; // YOUR GCP PROJECT ID IN ANDROID + if (await _appAttestationPlugin.getAttestationServiceSupport( + challengeString: sessionId, gcp: gpc) + case final tokenReceived?) { + // Try to parse the JSON response to extract keyID + try { + final Map response = jsonDecode(tokenReceived); + final String? keyID = response['keyID'] as String?; + + setState(() { + _tokenExample = tokenReceived; + if (keyID != null) { + _keyID = keyID; + } + }); + } catch (e) { + // If parsing fails, just store the token + setState(() { + _tokenExample = tokenReceived; + }); + } + } + } on PlatformException { + debugPrint('Failed to get token.'); + } + } + + Future generateAssertionExample() async { + if (_keyID.isEmpty) { + setState(() { + _assertionExample = 'No keyID available. Run attestation first.'; + }); + return; + } + + try { + // Example client data (will be hashed with SHA256 internally) + // This should be unique, single-use data block (e.g., timestamp + nonce + request data) + final clientData = + 'unique-request-${DateTime.now().millisecondsSinceEpoch}'; + + if (await _appAttestationPlugin.generateAssertion( + keyID: _keyID, clientData: clientData) + case final assertion?) { setState(() { - _tokenExample = tokenReceived; + _assertionExample = assertion; }); - return; } - tokenReceived = await _appAttestationPlugin.getAttestationServiceSupport( - challengeString: sessionId); + } on PlatformException catch (e) { + debugPrint('Failed to generate assertion: ${e.message}'); setState(() { - _tokenExample = tokenReceived; + _assertionExample = 'ERROR: ${e.message}'; }); - } on PlatformException { - tokenReceived = 'Failed to get token'; } } @@ -59,11 +102,39 @@ class _MyAppState extends State { title: const Text('Plugin example app'), ), body: Center( - child: GestureDetector( - onTap: () async { - await Clipboard.setData(ClipboardData(text: _tokenExample)); - }, - child: Text('Running token: $_tokenExample')), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: () async { + await Clipboard.setData(ClipboardData(text: _tokenExample)); + }, + child: Text( + 'Attestation Token: $_tokenExample', + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: generateAssertionExample, + child: const Text('Generate Assertion'), + ), + const SizedBox(height: 20), + GestureDetector( + onTap: () async { + await Clipboard.setData( + ClipboardData(text: _assertionExample)); + }, + child: Text( + 'Assertion: $_assertionExample', + textAlign: TextAlign.center, + ), + ), + ], + ), + ), ), ), ); diff --git a/example/pubspec.lock b/example/pubspec.lock index 9eb2727..5bee1e3 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: path: ".." relative: true source: path - version: "1.0.4" + version: "1.2.0" async: dependency: transitive description: @@ -44,18 +44,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.19.0" dio: dependency: "direct main" description: @@ -125,26 +117,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: @@ -165,18 +157,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" path: dependency: transitive description: @@ -189,10 +181,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -213,7 +205,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: @@ -226,10 +218,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -242,10 +234,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" sync_http: dependency: transitive description: @@ -266,10 +258,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.3" typed_data: dependency: transitive description: @@ -290,18 +282,18 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.3.0" webdriver: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.6.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3c786a7..5691e2e 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -7,28 +7,12 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: sdk: '>=3.2.0 <4.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 app_device_integrity: - # When depending on this package from a real application you should use: - # app_device_integrity: ^x.y.z - # See https://dart.dev/tools/pub/dependencies#version-constraints - # The example app is bundled with the plugin so we use a path dependency on - # the parent directory to use the current plugin's version. path: ../ - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - dio: ^5.3.4 dev_dependencies: @@ -36,52 +20,7 @@ dev_dependencies: sdk: flutter 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 + uses-material-design: true \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore index 0c88507..c7449a2 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -7,6 +7,8 @@ *.swp profile +.build/ +.swiftpm/ DerivedData/ build/ GeneratedPluginRegistrant.h diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/ios/Classes/AppDeviceIntegrity.swift b/ios/Classes/AppDeviceIntegrity.swift deleted file mode 100644 index 479d94c..0000000 --- a/ios/Classes/AppDeviceIntegrity.swift +++ /dev/null @@ -1,78 +0,0 @@ -import CryptoKit -import DeviceCheck -import Foundation - -@available(iOS 14.0, *) -final class AppDeviceIntegrity { - let inputString: String - var attestationString: String? - private let keyName = "AppAttestKeyIdentifier" - private let attestService = DCAppAttestService.shared - private var keyID: String? - - init?(challengeString: String) { - self.inputString = challengeString - - guard attestService.isSupported else { - print("[!] Attest service not available") - return nil - } - } - - func generateKeyAndAttest(completion: @escaping (Bool) -> Void) { - attestService.generateKey { [weak self] keyIdentifier, error in - guard let self = self else { return } - - if let error = error { - print("Key generation error: \(error.localizedDescription)") - completion(false) - return - } - - guard let keyIdentifier = keyIdentifier else { - print("Failed to generate key identifier") - completion(false) - return - } - - self.keyID = keyIdentifier - self.preAttestation(completion: completion) - } - } - - func keyIdentifier() -> String { - return ("\(self.keyID ?? "Error in Key ID")") - } - - // Pre-attestation process - private func preAttestation(completion: @escaping (Bool) -> Void) { - guard let keyID = self.keyID else { - print("No key ID available for attestation") - completion(false) - return - } - - let challenge = Data(self.inputString.utf8) - let hash = Data(SHA256.hash(data: challenge)) - - attestService.attestKey(keyID, clientDataHash: hash) { [weak self] attestation, error in - guard let self = self else { return } - - if let error = error { - print("Attestation error: \(error.localizedDescription)") - completion(false) - return - } - - guard let attestationObject = attestation else { - print("No attestation object received") - completion(false) - return - } - - self.attestationString = attestationObject.base64EncodedString() - print("Attestation successful") - completion(true) - } - } -} diff --git a/ios/app_device_integrity.podspec b/ios/app_device_integrity.podspec index 001367a..3851453 100644 --- a/ios/app_device_integrity.podspec +++ b/ios/app_device_integrity.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'app_device_integrity' - s.version = '0.0.1' + s.version = '0.0.2' s.summary = 'A new Flutter plugin project.' s.description = <<-DESC A new Flutter plugin project. @@ -13,9 +13,10 @@ A new Flutter plugin project. s.license = { :file => '../LICENSE' } s.author = { 'Your Company' => 'email@example.com' } s.source = { :path => '.' } - s.source_files = 'Classes/**/*' + s.source_files = 'app_device_integrity/Sources/app_device_integrity/**/*.swift' + s.resource_bundles = {'app_device_integrity_privacy' => ['app_device_integrity/Sources/app_device_integrity/PrivacyInfo.xcprivacy']} s.dependency 'Flutter' - s.platform = :ios, '11.0' + s.platform = :ios, '16.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/ios/app_device_integrity/Package.swift b/ios/app_device_integrity/Package.swift new file mode 100644 index 0000000..6b68f37 --- /dev/null +++ b/ios/app_device_integrity/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "app_device_integrity", + platforms: [ + .iOS(.v16) + ], + products: [ + .library(name: "app-device-integrity", type: .static, targets: ["app_device_integrity"]) + ], + dependencies: [], + targets: [ + .target( + name: "app_device_integrity", + dependencies: [], + resources: [ + .process("PrivacyInfo.xcprivacy") + ] + ) + ] +) + diff --git a/ios/app_device_integrity/Sources/app_device_integrity/AppDeviceIntegrity.swift b/ios/app_device_integrity/Sources/app_device_integrity/AppDeviceIntegrity.swift new file mode 100644 index 0000000..b6cd8bd --- /dev/null +++ b/ios/app_device_integrity/Sources/app_device_integrity/AppDeviceIntegrity.swift @@ -0,0 +1,153 @@ +import CryptoKit +import DeviceCheck +import Foundation + +final class AppDeviceIntegrity { + let inputString: String + var attestationString: String? + private let keyName = "AppAttestKeyIdentifier" + private let attestService = DCAppAttestService.shared + private var keyID: String? + + init?(challengeString: String) { + self.inputString = challengeString + + guard attestService.isSupported else { + print("[!] Attest service not available") + return nil + } + } + + init?(keyID: String) { + self.inputString = "" + self.keyID = keyID + + guard attestService.isSupported else { + print("[!] Attest service not available") + return nil + } + } + + func generateKeyAndAttest(completion: @escaping (Bool) -> Void) { + attestService.generateKey { [weak self] keyIdentifier, error in + guard let self = self else { return } + + if let error = error { + print("Key generation error: \(error.localizedDescription)") + completion(false) + return + } + + guard let keyIdentifier = keyIdentifier else { + print("Failed to generate key identifier") + completion(false) + return + } + + self.keyID = keyIdentifier + self.preAttestation(completion: completion) + } + } + + func keyIdentifier() -> String { + return ("\(self.keyID ?? "Error in Key ID")") + } + + // Pre-attestation process + private func preAttestation(completion: @escaping (Bool) -> Void) { + guard let keyID = self.keyID else { + print("No key ID available for attestation") + completion(false) + return + } + + // Decode Base64URL challenge to Data if input is Base64URL encoded + let challengeData: Data + if let base64Data = Data(base64Encoded: inputString) { + challengeData = base64Data + } else { + challengeData = Data(inputString.utf8) + } + + // let challenge = Data(self.inputString.utf8) + let hash = Data(SHA256.hash(data: challengeData)) + + attestService.attestKey(keyID, clientDataHash: hash) { [weak self] attestation, error in + guard let self = self else { return } + + if let error = error { + print("Attestation error: \(error.localizedDescription)") + completion(false) + return + } + + guard let attestationObject = attestation else { + print("No attestation object received") + completion(false) + return + } + + self.attestationString = attestationObject.base64EncodedString() + print("Attestation successful", attestationObject) + completion(true) + } + } + + // Generate assertion for an existing key + // clientData: Raw client data string that will be hashed with SHA256 + func generateAssertion(clientData: String, completion: @escaping (String?, Error?) -> Void) { + guard let keyID = self.keyID else { + let error = NSError( + domain: "AppDeviceIntegrity", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "No key ID available for assertion"] + ) + completion(nil, error) + return + } + + // Decode Base64URL challenge to Data if input is Base64URL encoded + let clientDataBytes: Data + if let base64Data = Data(base64Encoded: clientData) { + clientDataBytes = base64Data + } else { + clientDataBytes = Data(clientData.utf8) + } + + // Convert client data to Data and hash it with SHA256 + let clientDataHash = Data(SHA256.hash(data: clientDataBytes)) + + // Verify hash is at least 16 bytes as per Apple documentation + guard clientDataHash.count >= 16 else { + let error = NSError( + domain: "AppDeviceIntegrity", + code: -2, + userInfo: [NSLocalizedDescriptionKey: "Client data too short - SHA256 hash must be at least 16 bytes"] + ) + completion(nil, error) + return + } + + attestService.generateAssertion(keyID, clientDataHash: clientDataHash) { assertion, error in + if let error = error { + print("Assertion generation error: \(error.localizedDescription)") + completion(nil, error) + return + } + + guard let assertionData = assertion else { + let error = NSError( + domain: "AppDeviceIntegrity", + code: -3, + userInfo: [NSLocalizedDescriptionKey: "No assertion data received"] + ) + completion(nil, error) + return + } + + let assertionString = assertionData.base64EncodedString() + print("Assertion generated successfully", assertionString) + completion(assertionString, nil) + } + } +} diff --git a/ios/Classes/AppDeviceIntegrityPlugin.swift b/ios/app_device_integrity/Sources/app_device_integrity/AppDeviceIntegrityPlugin.swift similarity index 65% rename from ios/Classes/AppDeviceIntegrityPlugin.swift rename to ios/app_device_integrity/Sources/app_device_integrity/AppDeviceIntegrityPlugin.swift index a20f2be..f197bd4 100644 --- a/ios/Classes/AppDeviceIntegrityPlugin.swift +++ b/ios/app_device_integrity/Sources/app_device_integrity/AppDeviceIntegrityPlugin.swift @@ -1,7 +1,6 @@ import Flutter import UIKit -@available(iOS 14.0, *) public class AppDeviceIntegrityPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "app_attestation", binaryMessenger: registrar.messenger()) @@ -51,6 +50,36 @@ public class AppDeviceIntegrityPlugin: NSObject, FlutterPlugin { } } + case "generateAssertion": + guard let args = call.arguments as? [String: Any], + let keyID = args["keyID"] as? String, + let clientData = args["clientData"] as? String + else { + result(FlutterError(code: "-3", message: "Invalid arguments", details: nil)) + return + } + + guard let deviceIntegrity = AppDeviceIntegrity(keyID: keyID) else { + result(FlutterError(code: "-4", message: "Failed to initialize AppDeviceIntegrity", details: nil)) + return + } + + deviceIntegrity.generateAssertion(clientData: clientData) { assertionString, error in + DispatchQueue.main.async { + if let error = error { + result(FlutterError( + code: "-8", + message: "Assertion generation failed: \(error.localizedDescription)", + details: nil + )) + } else if let assertionString = assertionString { + result(assertionString) + } else { + result(FlutterError(code: "-9", message: "Unknown assertion generation error", details: nil)) + } + } + } + default: result(FlutterMethodNotImplemented) } diff --git a/ios/app_device_integrity/Sources/app_device_integrity/PrivacyInfo.xcprivacy b/ios/app_device_integrity/Sources/app_device_integrity/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..37865e7 --- /dev/null +++ b/ios/app_device_integrity/Sources/app_device_integrity/PrivacyInfo.xcprivacy @@ -0,0 +1,22 @@ + + + + + NSPrivacyTracking + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + + + + + + diff --git a/lib/app_device_integrity.dart b/lib/app_device_integrity.dart index 916683c..fd4924f 100644 --- a/lib/app_device_integrity.dart +++ b/lib/app_device_integrity.dart @@ -1,16 +1,51 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + import 'app_device_integrity_platform_interface.dart'; -class AppDeviceIntegrity { - Future getAttestationServiceSupport( - {required String challengeString, int? gcp}) { - if (Platform.isAndroid) { - return AppDeviceIntegrityPlatform.instance.getAttestationServiceSupport( - challengeString: challengeString, gcp: gcp!); +final class AppDeviceIntegrity { + const AppDeviceIntegrity(); + + Future getAttestationServiceSupport({ + required String challengeString, + int? gcp, + }) { + assert( + !Platform.isAndroid || gcp != null, + 'gcp parameter is required for Android platform', + ); + + return AppDeviceIntegrityPlatform.instance.getAttestationServiceSupport( + challengeString: challengeString, gcp: gcp); + } + + /// Generates an assertion for an existing key. + /// + /// **iOS only** - This method is only supported on iOS platform. + /// + /// [keyID] - The identifier received when generating a cryptographic key. + /// [clientData] - Unique, single-use data block that will be hashed with SHA256 + /// and signed with the attested private key. Should be at least 16 bytes. + /// + /// Returns a base64-encoded assertion object to send to your server for verification. + /// + /// Throws [PlatformException] if called on non-iOS platform. + Future generateAssertion({ + required String keyID, + required String clientData, + }) { + if (defaultTargetPlatform != TargetPlatform.iOS) { + throw PlatformException( + code: 'UNSUPPORTED_PLATFORM', + message: 'generateAssertion is only supported on iOS platform', + ); } - return AppDeviceIntegrityPlatform.instance - .getAttestationServiceSupport(challengeString: challengeString); + return AppDeviceIntegrityPlatform.instance.generateAssertion( + keyID: keyID, + clientData: clientData, + ); } } diff --git a/lib/app_device_integrity_method_channel.dart b/lib/app_device_integrity_method_channel.dart index bee074b..2a6ea7e 100644 --- a/lib/app_device_integrity_method_channel.dart +++ b/lib/app_device_integrity_method_channel.dart @@ -4,17 +4,31 @@ import 'package:flutter/services.dart'; import 'app_device_integrity_platform_interface.dart'; /// An implementation of [AppDeviceIntegrityPlatform] that uses method channels. -class MethodChannelAppDeviceIntegrity extends AppDeviceIntegrityPlatform { +final class MethodChannelAppDeviceIntegrity + implements AppDeviceIntegrityPlatform { /// The method channel used to interact with the native platform. @visibleForTesting final methodChannel = const MethodChannel('app_attestation'); @override - Future getAttestationServiceSupport( - {required String challengeString, int? gcp}) async { - final token = await methodChannel.invokeMethod( - 'getAttestationServiceSupport', - {'challengeString': challengeString, 'gcp': gcp}); - return token; + Future getAttestationServiceSupport({ + required String challengeString, + int? gcp, + }) { + return methodChannel.invokeMethod('getAttestationServiceSupport', { + 'challengeString': challengeString, + 'gcp': gcp, + }); + } + + @override + Future generateAssertion({ + required String keyID, + required String clientData, + }) { + return methodChannel.invokeMethod('generateAssertion', { + 'keyID': keyID, + 'clientData': clientData, + }); } } diff --git a/lib/app_device_integrity_platform_interface.dart b/lib/app_device_integrity_platform_interface.dart index 61a0e06..5917c66 100644 --- a/lib/app_device_integrity_platform_interface.dart +++ b/lib/app_device_integrity_platform_interface.dart @@ -2,7 +2,7 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'app_device_integrity_method_channel.dart'; -abstract class AppDeviceIntegrityPlatform extends PlatformInterface { +abstract interface class AppDeviceIntegrityPlatform extends PlatformInterface { /// Constructs a AppAttestationPlatform. AppDeviceIntegrityPlatform() : super(token: _token); @@ -24,8 +24,13 @@ abstract class AppDeviceIntegrityPlatform extends PlatformInterface { _instance = instance; } - Future getAttestationServiceSupport( - {required String challengeString, int? gcp}) { - throw UnimplementedError('platformVersion() has not been implemented.'); - } + Future getAttestationServiceSupport({ + required String challengeString, + int? gcp, + }); + + Future generateAssertion({ + required String keyID, + required String clientData, + }); } diff --git a/pubspec.yaml b/pubspec.yaml index 804c39a..0b118a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: app_device_integrity description: "This plugin simplifies app attestation by using Apple's App Attest and Google's Play Integrity to generate tokens for your Server to decrypt and verify reliable device access." -version: 1.1.0 +version: 1.2.0 homepage: https://bubotech.club environment: - sdk: '>=3.2.0 <4.0.0' - flutter: '>=3.3.0' + sdk: '>=3.6.0 <4.0.0' + flutter: '>=3.27.0' dependencies: flutter: diff --git a/test/app_device_integrity_method_channel_test.dart b/test/app_device_integrity_method_channel_test.dart index 2e1df4a..fd337c9 100644 --- a/test/app_device_integrity_method_channel_test.dart +++ b/test/app_device_integrity_method_channel_test.dart @@ -1,19 +1,25 @@ +import 'package:app_device_integrity/app_device_integrity_method_channel.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:app_device_integrity/app_device_integrity_method_channel.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - MethodChannelAppDeviceIntegrity platform = MethodChannelAppDeviceIntegrity(); - const MethodChannel channel = MethodChannel('app_device_integrity'); + final platform = MethodChannelAppDeviceIntegrity(); + const channel = MethodChannel('app_attestation'); setUp(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler( channel, (MethodCall methodCall) async { - return '42'; + if (methodCall.method == 'getAttestationServiceSupport') { + return 'UUID_RESPONSE'; + } + if (methodCall.method == 'generateAssertion') { + return 'ASSERTION_RESPONSE'; + } + return null; }, ); }); @@ -29,4 +35,11 @@ void main() { challengeString: 'UUID_TEST'), 'UUID_RESPONSE'); }); + + test('generateAssertion', () async { + expect( + await platform.generateAssertion( + keyID: 'TEST_KEY_ID', clientData: 'test client data for hashing'), + 'ASSERTION_RESPONSE'); + }); } diff --git a/test/app_device_integrity_test.dart b/test/app_device_integrity_test.dart index c99c26e..b47ad24 100644 --- a/test/app_device_integrity_test.dart +++ b/test/app_device_integrity_test.dart @@ -1,16 +1,26 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:app_device_integrity/app_device_integrity.dart'; -import 'package:app_device_integrity/app_device_integrity_platform_interface.dart'; import 'package:app_device_integrity/app_device_integrity_method_channel.dart'; +import 'package:app_device_integrity/app_device_integrity_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; class MockAppDeviceIntegrityPlatform with MockPlatformInterfaceMixin implements AppDeviceIntegrityPlatform { @override - Future getAttestationServiceSupport( - {required String challengeString, int? gcp}) => - Future.value('UUID_RESPONSE'); + Future getAttestationServiceSupport({ + required String challengeString, + int? gcp, + }) => + Future.value('42'); + + @override + Future generateAssertion({ + required String keyID, + required String clientData, + }) => + Future.value('mock_assertion_response'); } void main() { @@ -22,7 +32,7 @@ void main() { }); test('getAttestationServiceSupport', () async { - AppDeviceIntegrity appDeviceIntegrityPlugin = AppDeviceIntegrity(); + AppDeviceIntegrity appDeviceIntegrityPlugin = const AppDeviceIntegrity(); MockAppDeviceIntegrityPlatform fakePlatform = MockAppDeviceIntegrityPlatform(); AppDeviceIntegrityPlatform.instance = fakePlatform; @@ -32,4 +42,22 @@ void main() { challengeString: 'UUID_TEST'), '42'); }); + + test('generateAssertion', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + const appDeviceIntegrityPlugin = AppDeviceIntegrity(); + final fakePlatform = MockAppDeviceIntegrityPlatform(); + AppDeviceIntegrityPlatform.instance = fakePlatform; + + expect( + await appDeviceIntegrityPlugin.generateAssertion( + keyID: 'TEST_KEY_ID', + clientData: 'test client data for hashing', + ), + 'mock_assertion_response', + ); + + debugDefaultTargetPlatformOverride = null; + }); } From 1b0765e21499f6128522f50f2245bee0e8080787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awomir=20Kro=CC=81l?= Date: Wed, 10 Dec 2025 11:19:59 +0100 Subject: [PATCH 2/2] Update README and CHANGELOG --- CHANGELOG.md | 11 +++ README.md | 268 ++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 243 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 866efc8..312daf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.2.0 + +* Added Swift Package Manager (SPM) support for iOS +* Implemented `generateAssertion` API for iOS App Attest +* Added automatic Base64URL challenge detection and decoding +* Enhanced error handling with detailed error codes +* Migrated example project from CocoaPods to SPM +* Updated Swift code with proper completion handlers +* Improved documentation with comprehensive API examples +* Added assertion generation for validating previously attested devices + ## 1.1.0 * Updating Swift Implementation - contrib. [@Nebojsa92](https://github.com/Nebojsa92) diff --git a/README.md b/README.md index c889159..ca61ade 100644 --- a/README.md +++ b/README.md @@ -8,64 +8,260 @@

-This plugin was created to make your app attestation more easy. It uses the Native Attestation Providers from Apple and Google, App Attest and Play Integrity respectively, to generate tokens to be decrypted by your Server to check if your app is being accessed by a reliable device. +This plugin simplifies app attestation by using Apple's App Attest and Google's Play Integrity to generate tokens for your server to verify device integrity. It ensures your app is being accessed by a reliable, non-compromised device. -## How to Use It +## Features + +- **iOS App Attest Integration**: Generate attestation tokens and assertions using Apple's App Attest framework +- **Android Play Integrity**: Support for Google Play Integrity API +- **Base64URL Challenge Support**: Automatically handles both plain text and Base64URL encoded challenges +- **Assertion Generation**: Validate previously attested devices with assertion API (iOS only) +- **Swift Package Manager Support**: Modern iOS dependency management (SPM compatible) + +## Platform Support + +| Platform | Attestation | Assertion | Min Version | +|----------|------------|-----------|-------------| +| iOS | ✓ | ✓ | iOS 16.0+ | +| Android | ✓ | - | API 19+ | -To use this plugin correctly, you need to contemplate these two steps: +## Installation -Provide a Session UUID, in iOS Case
-Provide your GCP Project ID, in Android Case
+Add to your `pubspec.yaml`: +```yaml +dependencies: + app_device_integrity: ^1.2.0 +``` + +## How to Use It + +### Initial Setup **iOS:**
-The Session UUID is the challenge created by your server for App Attest to issue the token requested from the device to be "marked".
-Basically your server sends the challenge to Apple to ensure that the token is marked with it, attested by the service with it so that when the server receives the token, it was actually sent by the device with that session UUID sent by the server before. +The challenge is a unique session identifier created by your server for App Attest. This can be either: +- Plain text string (will be converted to UTF-8 bytes and hashed) +- Base64URL encoded string (will be automatically detected and decoded) + +The plugin automatically handles both formats. Your server sends the challenge to Apple to ensure the attestation token is cryptographically bound to it. This prevents replay attacks and ensures the token was generated for this specific session. **Android:**
-Providing the GCP Project ID links your app with your development and deployment environment. Before you implement it in your project, you need to follow the steps provided by the following doc:
-https://developer.android.com/google/play/integrity/setup?set-google-console#set-google-console +You need to provide your GCP (Google Cloud Platform) Project ID to link your app with your development environment. + +Before implementing in your project, follow these steps: +1. Visit [Google Play Integrity Setup](https://developer.android.com/google/play/integrity/setup?set-google-console#set-google-console) +2. Link your GCP Project to your app in Google Play Console +3. Find your Project ID next to your project name in the GCP Console +4. It's recommended to store the Project ID as an environment variable -To be more "precise", when you link you GCP Project to your app in Google Play Console, the project ID is right beside of your project name. That's the information you need.
-It's recommended to create environmental variable with the project ID to maintain your app, and project, integrity. +## Implementation -### How To Implement -After you import the plugin to your project, implement the token generation using the following steps: +### 1. Generate Initial Attestation Token + +Use this when first attesting a device: ```dart +import 'package:app_device_integrity/app_device_integrity.dart'; + +final deviceIntegrity = AppDeviceIntegrity(); -// Instance the way you prefer -final _appAttestationPlugin = AppDeviceIntegrity(); +// iOS Example +Future attestDeviceIOS() async { + try { + // Challenge from your server (can be plain text or Base64URL) + final sessionId = '550e8400-e29b-41d4-a716-446655440000'; + + final tokenJson = await deviceIntegrity.getAttestationServiceSupport( + challengeString: sessionId, + ); + + // Parse the JSON response + final token = jsonDecode(tokenJson!); + final attestationString = token['attestationString']; + final keyID = token['keyID']; + + // Send both to your server for verification + await sendToServer(attestationString, keyID); + } catch (e) { + print('Attestation failed: $e'); + } +} -// You need to provide -// An UUID generated by your server (you can customize the rules to generate it) -// In case the platform is Android, the GCP Project ID needs to be informed. -// For a more practical implementation, check the plugin example. +// Android Example +Future attestDeviceAndroid() async { + try { + final sessionId = '550e8400-e29b-41d4-a716-446655440000'; + const gcpProjectId = 123456789; // Your GCP Project ID + + final token = await deviceIntegrity.getAttestationServiceSupport( + challengeString: sessionId, + gcp: gcpProjectId, + ); + + // Send to your server for verification + await sendToServer(token); + } catch (e) { + print('Attestation failed: $e'); + } +} +``` -String sessionId = '550e8400-e29b-41d4-a716-446655440000'; -int gpc = 0000000000; // YOUR GCP PROJECT ID IN ANDROID +### 2. Generate Assertion for Previously Attested Device (iOS Only) -if (Platform.isAndroid) { +After initial attestation, use assertions to validate the device without re-attesting: + +```dart +Future generateAssertion() async { + try { + final deviceIntegrity = AppDeviceIntegrity(); + + // Use the keyID from initial attestation + final keyID = 'saved_key_id_from_initial_attestation'; + + // Client data to sign (can be plain text or Base64URL) + // Should be at least 16 bytes when hashed with SHA256 + final clientData = 'unique_session_data_${DateTime.now().millisecondsSinceEpoch}'; + + final assertionString = await deviceIntegrity.generateAssertion( + keyID: keyID, + clientData: clientData, + ); + + // Send assertion to your server for verification + await verifyAssertion(assertionString, clientData); + } on PlatformException catch (e) { + if (e.code == 'UNSUPPORTED_PLATFORM') { + print('Assertions are only supported on iOS'); + } else { + print('Assertion generation failed: ${e.message}'); + } + } +} +``` + +### Platform-Specific Implementation + +```dart +import 'dart:io'; + +Future attestDevice() async { + final deviceIntegrity = AppDeviceIntegrity(); + final sessionId = await getSessionIdFromServer(); + if (Platform.isAndroid) { + const gcpProjectId = 123456789; + final token = await deviceIntegrity.getAttestationServiceSupport( + challengeString: sessionId, + gcp: gcpProjectId, + ); + return sendToServer(token); + } - tokenReceived = await _appAttestationPlugin - .getAttestationServiceSupport(challengeString: sessionId, gcp: gpc); + if (Platform.isIOS) { + final tokenJson = await deviceIntegrity.getAttestationServiceSupport( + challengeString: sessionId, + ); + final token = jsonDecode(tokenJson!); + return sendToServer(token['attestationString'], token['keyID']); + } +} +``` - return; - -} +## Server-Side Verification + +I provide an open-source server implementation for token verification: +[App Device Integrity Server](https://github.com/Erluan/app_device_integrity_server) + +The server: +- Detects platform (iOS/Android) from the token +- Verifies tokens with Apple/Google services +- Provides risk assessment and device integrity information +- Helps make security decisions based on device trustworthiness + +Feel free to fork, clone, and customize it according to your business requirements. -tokenReceived = await _appAttestationPlugin - .getAttestationServiceSupport(challengeString: sessionId); - -return; +## API Reference + +### `getAttestationServiceSupport` + +Generates an attestation token for the device. + +**Parameters:** +- `challengeString` (required): Session identifier from your server (plain text or Base64URL) +- `gcp` (Android only): Google Cloud Platform Project ID + +**Returns:** +- iOS: JSON string with `attestationString` and `keyID` +- Android: Attestation token string + +**Example:** +```dart +final token = await deviceIntegrity.getAttestationServiceSupport( + challengeString: sessionId, + gcp: 123456789, // Android only +); +``` + +### `generateAssertion` (iOS Only) + +Generates an assertion for a previously attested device. + +**Parameters:** +- `keyID` (required): Key identifier from initial attestation +- `clientData` (required): Unique data to sign (min 16 bytes when hashed) + +**Returns:** +- Base64-encoded assertion string + +**Example:** +```dart +final assertion = await deviceIntegrity.generateAssertion( + keyID: savedKeyID, + clientData: sessionData, +); +``` + +**Throws:** +- `PlatformException` with code `UNSUPPORTED_PLATFORM` on non-iOS platforms + +## Error Handling + +```dart +try { + final token = await deviceIntegrity.getAttestationServiceSupport( + challengeString: sessionId, + ); +} on PlatformException catch (e) { + switch (e.code) { + case '-3': + print('Invalid arguments'); + case '-4': + print('Device does not support attestation (simulator or old device)'); + case '-5': + print('Attestation failed'); + default: + print('Error: ${e.message}'); + } +} ``` -**Server:**
-I am very proud to provide you with an open-source service that you can [Attest your implementations in server side](https://github.com/Erluan/app_device_integrity_server).
-It checks from which platform the token is sent and verifies the token, providing very important information about risks, enabling better decision-making to reinforce the security of your services.
-Feel comfortable to fork it, or clone it, and customize it, according to your business demands. +## Additional Resources -For more information about App Attest and Play Integrity, you can access the docs from Apple and Google in the links bellow:
+**Apple App Attest:**
https://developer.apple.com/documentation/devicecheck/establishing-your-app-s-integrity + +**Google Play Integrity:**
https://developer.android.com/google/play/integrity +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## Support + +For issues and questions: +- GitHub Issues: [Create an issue](https://github.com/Erluan/app_device_integrity/issues) +- Discord: [Join our server](https://discord.gg/8GEp4dgM) + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.