diff --git a/melos.yaml b/melos.yaml
index 6b868b96..977c09cb 100644
--- a/melos.yaml
+++ b/melos.yaml
@@ -3,9 +3,6 @@ repository: https://github.com/corbado/flutter-passkeys
packages:
- packages/**
-command:
- bootstrap:
- usePubspecOverrides: true
scripts:
format:
diff --git a/packages/corbado_auth/pubspec.yaml b/packages/corbado_auth/pubspec.yaml
index 64bec2ab..a16f62f1 100644
--- a/packages/corbado_auth/pubspec.yaml
+++ b/packages/corbado_auth/pubspec.yaml
@@ -6,7 +6,7 @@ repository: https://github.com/corbado/flutter-passkeys/tree/main/packages/corba
version: 3.7.1
environment:
- sdk: ">=3.0.0 <4.0.0"
+ sdk: '>=3.8.0 <4.0.0'
flutter: ">=3.0.0"
dependencies:
@@ -18,7 +18,7 @@ dependencies:
sdk: flutter
flutter_secure_storage: "<=9.0.0 >8.0.0"
http: ^1.1.2
- json_annotation: ^4.8.1
+ json_annotation: ^4.10.0
jwt_decoder: ^2.0.1
meta: ^1.15.0
passkeys: ^2.13.0
diff --git a/packages/passkeys/passkeys/example/ios/Flutter/AppFrameworkInfo.plist b/packages/passkeys/passkeys/example/ios/Flutter/AppFrameworkInfo.plist
index 7c569640..1dc6cf76 100644
--- a/packages/passkeys/passkeys/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/packages/passkeys/passkeys/example/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 12.0
+ 13.0
diff --git a/packages/passkeys/passkeys/example/ios/Podfile b/packages/passkeys/passkeys/example/ios/Podfile
index 2c068c40..10f3c9b4 100644
--- a/packages/passkeys/passkeys/example/ios/Podfile
+++ b/packages/passkeys/passkeys/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-platform :ios, '12.0'
+platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/packages/passkeys/passkeys/example/ios/Runner.xcodeproj/project.pbxproj b/packages/passkeys/passkeys/example/ios/Runner.xcodeproj/project.pbxproj
index 4321e604..ae7f0f6d 100644
--- a/packages/passkeys/passkeys/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/passkeys/passkeys/example/ios/Runner.xcodeproj/project.pbxproj
@@ -141,6 +141,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
1F5231B1C0B4B24BD6457DFB /* [CP] Embed Pods Frameworks */,
+ 635C969084CC2623F9C369FA /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -235,6 +236,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
+ 635C969084CC2623F9C369FA /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -347,7 +365,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -430,7 +448,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -479,7 +497,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
diff --git a/packages/passkeys/passkeys/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/passkeys/passkeys/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index c53e2b31..9c12df59 100644
--- a/packages/passkeys/passkeys/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/passkeys/passkeys/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
canAuthenticate() {
return _platform.canAuthenticate();
}
@@ -40,7 +40,7 @@ class PasskeyAuthenticator {
/// Creates a new passkey and stores it on the device.
/// Returns [RegisterResponseType] which must be sent to the relying party
/// server.
- Future register(RegisterRequestType request) async {
+ Future register(RegisterRequestType request, {String? salt}) async {
if (debugMode) {
await _doctor.check(request.relyingParty.id);
}
@@ -56,7 +56,7 @@ class PasskeyAuthenticator {
_isValidCredentialID(credential.id);
}
- final r = await _platform.register(request);
+ final r = await _platform.register(request, salt);
return r;
} on PlatformException catch (e) {
@@ -92,9 +92,7 @@ class PasskeyAuthenticator {
/// Authenticates a user with a passkey.
/// Returns [AuthenticateResponseType] which must be sent to the relying party
/// server.
- Future authenticate(
- AuthenticateRequestType request,
- ) async {
+ Future authenticate(AuthenticateRequestType request, {String? salt}) async {
if (debugMode) {
await _doctor.check(request.relyingPartyId);
}
@@ -110,7 +108,7 @@ class PasskeyAuthenticator {
}
}
- final r = await _platform.authenticate(request);
+ final r = await _platform.authenticate(request, salt);
return r;
} on PlatformException catch (e) {
@@ -174,9 +172,7 @@ class PasskeyAuthenticator {
void _isValidChallenge(String challenge) {
if (!_isValidBase64Url(input: challenge)) {
if (debugMode) {
- _doctor.recordException(
- PlatformException(code: 'malformed-base64-url-challenge'),
- );
+ _doctor.recordException(PlatformException(code: 'malformed-base64-url-challenge'));
}
throw MalformedBase64UrlChallenge();
}
@@ -186,9 +182,7 @@ class PasskeyAuthenticator {
void _isValidCredentialID(String credentialID) {
if (!_isValidBase64Url(input: credentialID)) {
if (debugMode) {
- _doctor.recordException(
- PlatformException(code: 'malformed-base64-url-credential-id'),
- );
+ _doctor.recordException(PlatformException(code: 'malformed-base64-url-credential-id'));
}
throw MalformedBase64UrlCredentialID();
}
@@ -198,9 +192,7 @@ class PasskeyAuthenticator {
void _isValidUserID(String userID) {
if (!_isValidBase64Url(input: userID, allowPadding: true)) {
if (debugMode) {
- _doctor.recordException(
- PlatformException(code: 'malformed-base64-url-user-id'),
- );
+ _doctor.recordException(PlatformException(code: 'malformed-base64-url-user-id'));
}
throw MalformedBase64UrlUserID();
}
@@ -227,8 +219,7 @@ class PasskeyAuthenticator {
if (!base64UrlRegex.hasMatch(input)) return false;
try {
- String normalized =
- input.padRight(input.length + (4 - input.length % 4) % 4, '=');
+ String normalized = input.padRight(input.length + (4 - input.length % 4) % 4, '=');
base64Url.decode(normalized);
return true;
diff --git a/packages/passkeys/passkeys/pubspec.yaml b/packages/passkeys/passkeys/pubspec.yaml
index 30490664..b5f9bafc 100644
--- a/packages/passkeys/passkeys/pubspec.yaml
+++ b/packages/passkeys/passkeys/pubspec.yaml
@@ -3,9 +3,11 @@ description: Flutter plugin enabling simple passkey authentication. Can be eithe
homepage: https://docs.corbado.com/overview/welcome
repository: https://github.com/corbado/flutter-passkeys/tree/main/packages/passkeys/passkeys
version: 2.17.4
+publish_to: none
+
environment:
- sdk: ">=3.0.0 <4.0.0"
+ sdk: '>=3.8.0 <4.0.0'
flutter: ">=3.0.0"
flutter:
@@ -27,12 +29,18 @@ dependencies:
flutter:
sdk: flutter
json_annotation: ^4.8.1
- passkeys_android: ^2.11.0
- passkeys_darwin: ^0.3.0
- passkeys_doctor: ^1.2.0
- passkeys_platform_interface: ^2.6.0
- passkeys_web: ^2.8.1
- passkeys_windows: ^0.1.1
+ passkeys_android:
+ path: ../passkeys_android
+ passkeys_darwin:
+ path: ../passkeys_darwin
+ passkeys_doctor:
+ path: ../passkeys_doctor
+ passkeys_platform_interface:
+ path: ../passkeys_platform_interface
+ passkeys_web:
+ path: ../passkeys_web
+ passkeys_windows:
+ path: ../passkeys_windows
ua_client_hints: ^1.1.3
dev_dependencies:
@@ -43,3 +51,7 @@ dev_dependencies:
mocktail: ^1.0.0
plugin_platform_interface: ^2.0.0
very_good_analysis: ^5.0.0
+
+dependency_overrides:
+ passkeys_platform_interface:
+ path: ../passkeys_platform_interface
\ No newline at end of file
diff --git a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java
index 153b5339..458d301c 100644
--- a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java
+++ b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/MessageHandler.java
@@ -2,6 +2,7 @@
import android.app.Activity;
import android.os.CancellationSignal;
+import android.util.Base64;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -41,7 +42,9 @@
import org.json.JSONObject;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -89,7 +92,9 @@ public void register(
@Nullable Long timeout,
@Nullable String attestation,
@NonNull List excludeCredentials,
- @NonNull Messages.Result result) {
+ @Nullable String salt,
+ @NonNull Messages.Result result
+ ) {
if (android.os.Build.VERSION.SDK_INT < 28) {
result.error(new Messages.FlutterError("android-passkey-unsupported",
"Passkeys are only supported on Android API 28 and above.", null));
@@ -123,7 +128,24 @@ public void register(
excludeCredentialsType);
try {
- String options = createCredentialOptions.toJSON().toString();
+ JSONObject optionsJson = createCredentialOptions.toJSON();
+
+ // PRF extension if salt provided
+ if (salt != null && !salt.isEmpty()) {
+ String saltBase64Url = hexToBase64Url(salt);
+ JSONObject extensions = optionsJson.optJSONObject("extensions");
+ if (extensions == null) extensions = new JSONObject();
+
+ JSONObject prf = new JSONObject();
+ JSONObject eval = new JSONObject();
+ eval.put("first", saltBase64Url);
+ prf.put("eval", eval);
+
+ extensions.put("prf", prf);
+ optionsJson.put("extensions", extensions);
+ }
+ String options = optionsJson.toString();
+ Log.i("Passkeys", "options = " + options);
Activity activity = plugin.requireActivity();
CredentialManager credentialManager = CredentialManager.create(activity);
@@ -158,12 +180,32 @@ public void onResult(CreateCredentialResponse res) {
typedTransports.add("");
}
+ JSONObject ext = json.optJSONObject("clientExtensionResults");
+ Map extMap = null;
+
+ if (ext != null) {
+ extMap = new HashMap<>();
+
+ JSONObject prf = ext.optJSONObject("prf");
+ if (prf != null) {
+ JSONObject results = prf.optJSONObject("results");
+ if (results != null) {
+ String first = results.optString("first", "");
+ Map resultsMap = new HashMap<>();
+ resultsMap.put("first", first);
+ Map prfMap = new HashMap<>();
+ prfMap.put("results", resultsMap);
+ extMap.put("prf", prfMap);
+ }
+ }
+ }
result.success(new Messages.RegisterResponse.Builder()
.setId(json.getString("id"))
.setRawId(json.getString("rawId"))
.setClientDataJSON(response.getString("clientDataJSON"))
.setAttestationObject(response.getString("attestationObject"))
.setTransports(typedTransports)
+ .setClientExtensionResults(extMap)
.build());
} catch (JSONException e) {
Log.e(TAG, "Error parsing response: " + resp, e);
@@ -219,6 +261,7 @@ public void onError(CreateCredentialException e) {
public void authenticate(@NonNull String relyingPartyId, @NonNull String challenge, @Nullable Long timeout,
@Nullable String userVerification, @Nullable List allowCredentials,
@Nullable Boolean preferImmediatelyAvailableCredentials,
+ @Nullable String salt,
@NonNull Messages.Result result) {
if (android.os.Build.VERSION.SDK_INT < 28) {
result.error(new Messages.FlutterError("android-passkey-unsupported",
@@ -235,8 +278,24 @@ public void authenticate(@NonNull String relyingPartyId, @NonNull String challen
GetCredentialOptions getCredentialOptions = new GetCredentialOptions(challenge, timeout, relyingPartyId,
allowCredentialsType, userVerification);
try {
- String options = getCredentialOptions.toJSON().toString();
-
+ JSONObject optionsJson = getCredentialOptions.toJSON();
+ // PRF extension if salt provided
+ if (salt != null && !salt.isEmpty()) {
+ String saltBase64Url = hexToBase64Url(salt);
+ Log.i("Passkey", "salt provided auth" + saltBase64Url);
+ JSONObject extensions = optionsJson.optJSONObject("extensions");
+ if (extensions == null) extensions = new JSONObject();
+
+ JSONObject prf = new JSONObject();
+ JSONObject eval = new JSONObject();
+ eval.put("first", saltBase64Url);
+ prf.put("eval", eval);
+
+ extensions.put("prf", prf);
+ optionsJson.put("extensions", extensions);
+ }
+ String options = optionsJson.toString();
+ Log.i("Passkeys", "options = " + options);
Activity activity = plugin.requireActivity();
CredentialManager credentialManager = CredentialManager.create(activity);
@@ -275,9 +334,30 @@ public void onResult(GetCredentialResponse res) {
final String signature = response.getString("signature");
final String authenticatorData = response.getString("authenticatorData");
+ JSONObject ext = json.optJSONObject("clientExtensionResults");
+ Map extMap = null;
+
+ if (ext != null) {
+ extMap = new HashMap<>();
+
+ JSONObject prf = ext.optJSONObject("prf");
+ if (prf != null) {
+ JSONObject results = prf.optJSONObject("results");
+ if (results != null) {
+ String first = results.optString("first", "");
+ Map resultsMap = new HashMap<>();
+ resultsMap.put("first", first);
+ Map prfMap = new HashMap<>();
+ prfMap.put("results", resultsMap);
+ extMap.put("prf", prfMap);
+ }
+ }
+ }
final Messages.AuthenticateResponse msg = new Messages.AuthenticateResponse.Builder()
.setId(id).setRawId(rawId).setClientDataJSON(clientDataJSON)
- .setAuthenticatorData(authenticatorData).setSignature(signature)
+ .setAuthenticatorData(authenticatorData)
+ .setSignature(signature)
+ .setClientExtensionResults(extMap)
.setUserHandle(userHandle).build();
result.success(msg);
@@ -339,4 +419,17 @@ public void cancelCurrentAuthenticatorOperation(@NonNull Messages.Result r
result.success(null);
}
+
+ /// `hexToBase64Url`
+ public static String hexToBase64Url(String hex) {
+ int len = hex.length();
+ byte[] bytes = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ bytes[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
+ }
+ return Base64.encodeToString(
+ bytes,
+ Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP
+ );
+ }
}
diff --git a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/Messages.java b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/Messages.java
index 2b5e2bd1..bd49c769 100644
--- a/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/Messages.java
+++ b/packages/passkeys/passkeys_android/android/src/main/java/com/corbado/passkeys_android/Messages.java
@@ -722,6 +722,17 @@ public void setTransports(@NonNull List setterArg) {
this.transports = setterArg;
}
+ /** The clientExtensionResults - PRF results */
+ private @Nullable Map clientExtensionResults;
+
+ public @Nullable Map getClientExtensionResults() {
+ return clientExtensionResults;
+ }
+
+ public void setClientExtensionResults(@Nullable Map setterArg) {
+ this.clientExtensionResults = setterArg;
+ }
+
/** Constructor is non-public to enforce null safety; use Builder. */
RegisterResponse() {}
@@ -762,6 +773,13 @@ public static final class Builder {
return this;
}
+ private @Nullable Map clientExtensionResults;
+
+ public @NonNull Builder setClientExtensionResults(@Nullable Map setterArg) {
+ this.clientExtensionResults = setterArg;
+ return this;
+ }
+
public @NonNull RegisterResponse build() {
RegisterResponse pigeonReturn = new RegisterResponse();
pigeonReturn.setId(id);
@@ -769,18 +787,20 @@ public static final class Builder {
pigeonReturn.setClientDataJSON(clientDataJSON);
pigeonReturn.setAttestationObject(attestationObject);
pigeonReturn.setTransports(transports);
+ pigeonReturn.setClientExtensionResults(clientExtensionResults);
return pigeonReturn;
}
}
@NonNull
ArrayList