diff --git a/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/view/credential_manifest_credential_offer_pick_page.dart b/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/view/credential_manifest_credential_offer_pick_page.dart index 4e09d4476..060dc3868 100644 --- a/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/view/credential_manifest_credential_offer_pick_page.dart +++ b/lib/dashboard/home/tab_bar/credentials/present/pick/credential_manifest/view/credential_manifest_credential_offer_pick_page.dart @@ -287,7 +287,7 @@ class CredentialManifestOfferPickView extends StatelessWidget { return; } - await context.read().credentialOffer( + await context.read().credentialOfferOrPresent( uri: uri, credentialModel: credential, keyId: SecureStorageKeys.ssiKey, diff --git a/lib/dashboard/home/tab_bar/credentials/receive/view/credentials_receive_page.dart b/lib/dashboard/home/tab_bar/credentials/receive/view/credentials_receive_page.dart index f56d33ebf..7a9f152a1 100644 --- a/lib/dashboard/home/tab_bar/credentials/receive/view/credentials_receive_page.dart +++ b/lib/dashboard/home/tab_bar/credentials/receive/view/credentials_receive_page.dart @@ -139,13 +139,14 @@ class _CredentialsReceivePageState extends State { ), ); } else { - context.read().credentialOffer( - uri: widget.uri, - credentialModel: credentialModel, - keyId: SecureStorageKeys.ssiKey, - issuer: widget.issuer, - isFromPresentation: false, - ); + context.read().credentialOfferOrPresent( + uri: widget.uri, + credentialModel: credentialModel, + keyId: SecureStorageKeys.ssiKey, + issuer: widget.issuer, + isFromPresentation: false, + credentialsToBePresented: [], + ); } }, ), diff --git a/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart b/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart index c0f87753f..28d9a0c63 100644 --- a/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart +++ b/lib/dashboard/qr_code/qr_code_scan/cubit/qr_code_scan_cubit.dart @@ -188,6 +188,7 @@ class QRCodeScanCubit extends Cubit { if (!isOID4VCUrl) { emit(state.acceptHost(isRequestVerified: true)); + return; } /// SIOPV2 : wallet returns an id_token which is a simple jwt @@ -241,38 +242,8 @@ class QRCodeScanCubit extends Cubit { await launchSiopV2WithRequestUriAsValueFlow(); } else if (responseType == 'vp_token') { /// verifier side (oidc4vp) with request uri as value - final String? presentationDefinitionValue = - uri.queryParameters['presentation_definition']; - - if (presentationDefinitionValue == null) { - throw Exception(); - } - - final json = - jsonDecode(presentationDefinitionValue.replaceAll("'", '"')) - as Map; - - final PresentationDefinition presentationDefinition = - PresentationDefinition.fromJson(json); - - final CredentialManifest credentialManifest = CredentialManifest( - '', - IssuedBy('', ''), - [], - presentationDefinition, - ); - final isPresentable = await isVCPresentable(presentationDefinition); - if (!isPresentable) { - emit( - state.copyWith( - qrScanStatus: QrScanStatus.success, - route: MissingCredentialsPage.route( - credentialManifest: credentialManifest, - ), - ), - ); - return; - } + await launchOIDC4VPWithRequestUriAsValueFlow(); + return; } else { throw Exception(); } @@ -581,7 +552,7 @@ class QRCodeScanCubit extends Cubit { return isPresentable; } } - return false; + return true; } void navigateToOidc4vcCredentialPickPage(List credentials) { @@ -629,6 +600,72 @@ class QRCodeScanCubit extends Cubit { } } + Future launchOIDC4VPWithRequestUriAsValueFlow() async { + if (isUriAsValueValid && keys.contains('presentation_definition')) { + final String presentationDefinitionValue = + state.uri?.queryParameters['presentation_definition'] ?? ''; + + final json = jsonDecode(presentationDefinitionValue.replaceAll("'", '"')) + as Map; + + final PresentationDefinition presentationDefinition = + PresentationDefinition.fromJson(json); + + final CredentialManifest credentialManifest = CredentialManifest( + 'id', + IssuedBy('', ''), + null, + presentationDefinition, + ); + final isPresentable = await isVCPresentable(presentationDefinition); + if (!isPresentable) { + emit( + state.copyWith( + qrScanStatus: QrScanStatus.success, + route: MissingCredentialsPage.route( + credentialManifest: credentialManifest, + ), + ), + ); + return; + } + + final CredentialModel credentialPreview = CredentialModel( + id: 'id', + image: 'image', + credentialPreview: Credential.dummy(), + shareLink: 'shareLink', + display: Display.emptyDisplay(), + data: const {}, + credentialManifest: credentialManifest, + ); + + emit( + state.copyWith( + qrScanStatus: QrScanStatus.success, + route: CredentialManifestOfferPickPage.route( + uri: state.uri!, + credential: credentialPreview, + issuer: Issuer.emptyIssuer('domain'), + inputDescriptorIndex: 0, + credentialsToBePresented: [], + ), + ), + ); + } else { + emit( + state.error( + message: StateMessage.error( + messageHandler: ResponseMessage( + ResponseString + .RESPONSE_STRING_SOMETHING_WENT_WRONG_TRY_AGAIN_LATER, + ), + ), + ), + ); + } + } + Future launchOIDC4VPAndSiopV2RequestAsURIFlow() async { final requestUri = state.uri!.queryParameters['request_uri'].toString(); @@ -770,7 +807,7 @@ class QRCodeScanCubit extends Cubit { .toList(); final PresentationDefinition presentationDefinition = - PresentationDefinition(inputDescriptorList); + PresentationDefinition(inputDescriptors: inputDescriptorList); final CredentialModel credentialPreview = CredentialModel( id: 'id', image: 'image', diff --git a/lib/scan/cubit/scan_cubit.dart b/lib/scan/cubit/scan_cubit.dart index 339cae44b..8f8fe2120 100644 --- a/lib/scan/cubit/scan_cubit.dart +++ b/lib/scan/cubit/scan_cubit.dart @@ -50,11 +50,11 @@ class ScanCubit extends Cubit { final ProfileCubit profileCubit; final DIDCubit didCubit; - Future credentialOffer({ + Future credentialOfferOrPresent({ required Uri uri, required CredentialModel credentialModel, required String keyId, - List? credentialsToBePresented, + required List? credentialsToBePresented, required Issuer issuer, required bool isFromPresentation, }) async { @@ -63,84 +63,40 @@ class ScanCubit extends Cubit { final log = getLogger('ScanCubit - credentialOffer'); try { - if (uri.queryParameters['scope'] == 'openid' || - uri.toString().startsWith('openid://')) { - OIDC4VCType? currentOIIDC4VCType; - - if (isFromPresentation) { - currentOIIDC4VCType = profileCubit.state.model.oidc4vcType; - } else { - for (final oidc4vcType in OIDC4VCType.values) { - if (oidc4vcType.isEnabled && - uri.toString().startsWith(oidc4vcType.offerPrefix)) { - currentOIIDC4VCType = oidc4vcType; - } - } - } - - if (currentOIIDC4VCType == null) { - throw Exception(); - } - - final OIDC4VC oidc4vc = currentOIIDC4VCType.getOIDC4VC; - final mnemonic = - await getSecureStorage.get(SecureStorageKeys.ssiMnemonic); - final privateKey = await oidc4vc.privateKeyFromMnemonic( - mnemonic: mnemonic!, - index: currentOIIDC4VCType.index, - ); - - late String did; - late String kid; - - if (currentOIIDC4VCType.issuerVcType == 'ldp_vc') { - const didMethod = AltMeStrings.defaultDIDMethod; - did = didKitProvider.keyToDID(didMethod, privateKey); - kid = await didKitProvider.keyToVerificationMethod( - didMethod, - privateKey, + if (uri.toString().startsWith('openid')) { + final responseType = uri.queryParameters['response_type'] ?? ''; + if (uri.toString().startsWith('openid://')) { + await presentCredentialToOIDC4VPAndSiopV2Request( + credentialsToBePresented: credentialsToBePresented, + isFromPresentation: isFromPresentation, + issuer: issuer, + uri: uri, ); - } else if (currentOIIDC4VCType.issuerVcType == 'jwt_vc') { - final private = await oidc4vc.getPrivateKey(mnemonic, privateKey); - - final thumbprint = getThumbprint(private); - final encodedAddress = Base58Encode([2, ...thumbprint]); - did = 'did:ebsi:z$encodedAddress'; - final lastPart = Base58Encode(thumbprint); - kid = '$did#$lastPart'; - } else { - throw Exception(); + return; } - final credentialList = credentialsToBePresented! - .map((e) => jsonEncode(e.toJson())) - .toList(); - - await oidc4vc.sendPresentation( - uri, - credentialList, - null, - privateKey, - did, - kid, - ); - - await presentationActivity( - credentialModels: credentialsToBePresented, - issuer: issuer, - ); - - emit( - state.copyWith( - status: ScanStatus.success, - message: StateMessage.success( - messageHandler: ResponseMessage( - ResponseString - .RESPONSE_STRING_SUCCESSFULLY_PRESENTED_YOUR_CREDENTIAL, - ), - ), - ), - ); + if (uri.toString().startsWith('openid-vc://')) { + if (responseType == 'id_token') { + /// verifier side (siopv2) with request uri as value + throw Exception(); + } else if (responseType == 'vp_token') { + /// verifier side (oidc4vp) with request uri as value + + final redirectUri = uri.queryParameters['redirect_uri'] ?? ''; + final nonce = uri.queryParameters['nonce'] ?? ''; + await presentCredentialToOID4VPRequest( + issuer: issuer, + credentialsToBePresented: credentialsToBePresented, + nonce: nonce, + presentationDefinition: + credentialModel.credentialManifest!.presentationDefinition!, + redirectUri: redirectUri, + ); + return; + } else { + throw Exception(); + } + } } else { final did = (await secureStorageProvider.get(SecureStorageKeys.did))!; @@ -505,7 +461,7 @@ class ScanCubit extends Cubit { await Future.delayed(const Duration(milliseconds: 500)); try { final vpToken = await createVpToken( - credential: credential, + credentialsToBePresented: [credential], challenge: sIOPV2Param.nonce!, ); final idToken = await createIdToken(nonce: sIOPV2Param.nonce!); @@ -572,6 +528,199 @@ class ScanCubit extends Cubit { } } + Future presentCredentialToOIDC4VPAndSiopV2Request({ + required List? credentialsToBePresented, + required Issuer issuer, + required Uri uri, + required bool isFromPresentation, + }) async { + final log = + getLogger('ScanCubit - presentCredentialToOIDC4VPAndSiopV2Request'); + try { + OIDC4VCType? currentOIIDC4VCType; + + if (isFromPresentation) { + currentOIIDC4VCType = profileCubit.state.model.oidc4vcType; + } else { + for (final oidc4vcType in OIDC4VCType.values) { + if (oidc4vcType.isEnabled && + uri.toString().startsWith(oidc4vcType.offerPrefix)) { + currentOIIDC4VCType = oidc4vcType; + } + } + } + + if (currentOIIDC4VCType == null) { + throw Exception(); + } + + final OIDC4VC oidc4vc = currentOIIDC4VCType.getOIDC4VC; + final mnemonic = + await getSecureStorage.get(SecureStorageKeys.ssiMnemonic); + final privateKey = await oidc4vc.privateKeyFromMnemonic( + mnemonic: mnemonic!, + index: currentOIIDC4VCType.index, + ); + + late String did; + late String kid; + + if (currentOIIDC4VCType.issuerVcType == 'ldp_vc') { + const didMethod = AltMeStrings.defaultDIDMethod; + did = didKitProvider.keyToDID(didMethod, privateKey); + kid = await didKitProvider.keyToVerificationMethod( + didMethod, + privateKey, + ); + } else if (currentOIIDC4VCType.issuerVcType == 'jwt_vc') { + final private = await oidc4vc.getPrivateKey(mnemonic, privateKey); + + final thumbprint = getThumbprint(private); + final encodedAddress = Base58Encode([2, ...thumbprint]); + did = 'did:ebsi:z$encodedAddress'; + final lastPart = Base58Encode(thumbprint); + kid = '$did#$lastPart'; + } else { + throw Exception(); + } + + final credentialList = + credentialsToBePresented!.map((e) => jsonEncode(e.toJson())).toList(); + + await oidc4vc.sendPresentation( + uri, + credentialList, + null, + privateKey, + did, + kid, + ); + + await presentationActivity( + credentialModels: credentialsToBePresented, + issuer: issuer, + ); + + emit( + state.copyWith( + status: ScanStatus.success, + message: StateMessage.success( + messageHandler: ResponseMessage( + ResponseString + .RESPONSE_STRING_SUCCESSFULLY_PRESENTED_YOUR_CREDENTIAL, + ), + ), + ), + ); + } catch (e) { + log.e('something went wrong', e); + if (e is MessageHandler) { + emit( + state.error(messageHandler: e), + ); + } else { + emit( + state.error( + messageHandler: ResponseMessage( + ResponseString + .RESPONSE_STRING_SOMETHING_WENT_WRONG_TRY_AGAIN_LATER, // ignore: lines_longer_than_80_chars + ), + ), + ); + } + return; + } + } + + Future presentCredentialToOID4VPRequest({ + required List? credentialsToBePresented, + required PresentationDefinition presentationDefinition, + required String nonce, + required String redirectUri, + required Issuer issuer, + }) async { + final log = getLogger('ScanCubit - presentCredentialToOID4VPRequest'); + emit(state.loading()); + await Future.delayed(const Duration(milliseconds: 500)); + try { + final vpToken = await createVpToken( + credentialsToBePresented: credentialsToBePresented!, + challenge: nonce, + ); + + final uuid1 = const Uuid().v4(); + + final Map presentationSubmission = { + 'presentation_submission': { + 'id': uuid1, + 'definition_id': presentationDefinition.id, + } + }; + + final inputDescriptors = >[]; + + for (final inputDescriptor in presentationDefinition.inputDescriptors) { + inputDescriptors.add({ + 'id': inputDescriptor.id, + 'format': 'ldp_vc', // type of the VC + 'path': r'$.verifiableCredential' + }); + } + + presentationSubmission['descriptor_map'] = inputDescriptors; + + final result = await client.post( + redirectUri, + data: FormData.fromMap({ + 'vp_token': vpToken, + 'presentation_submission': presentationSubmission, + }), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + ); + + if (result['status_code'] == 200) { + await presentationActivity( + credentialModels: credentialsToBePresented, + issuer: issuer, + ); + emit( + state.copyWith( + status: ScanStatus.success, + message: StateMessage.success( + messageHandler: ResponseMessage( + ResponseString + .RESPONSE_STRING_SUCCESSFULLY_PRESENTED_YOUR_CREDENTIAL, + ), + ), + ), + ); + } else { + throw ResponseMessage( + ResponseString.RESPONSE_STRING_SOMETHING_WENT_WRONG_TRY_AGAIN_LATER, + ); + } + } catch (e) { + log.e('something went wrong', e); + if (e is MessageHandler) { + emit( + state.error(messageHandler: e), + ); + } else { + emit( + state.error( + messageHandler: ResponseMessage( + ResponseString + .RESPONSE_STRING_SOMETHING_WENT_WRONG_TRY_AGAIN_LATER, // ignore: lines_longer_than_80_chars + ), + ), + ); + } + return; + } + } + Future askPermissionDIDAuthCHAPI({ required String keyId, String? challenge, @@ -592,7 +741,7 @@ class ScanCubit extends Cubit { Future createVpToken({ required String challenge, - required CredentialModel credential, + required List credentialsToBePresented, }) async { final ssiKey = await secureStorageProvider.get(SecureStorageKeys.ssiKey); final did = await secureStorageProvider.get(SecureStorageKeys.did); @@ -609,7 +758,9 @@ class ScanCubit extends Cubit { 'type': ['VerifiablePresentation'], 'id': presentationId, 'holder': did, - 'verifiableCredential': credential.data, + 'verifiableCredential': credentialsToBePresented.length == 1 + ? credentialsToBePresented.first.data + : credentialsToBePresented.map((c) => c.data).toList(), }), options, ssiKey!, diff --git a/packages/credential_manifest/lib/src/models/input_descriptor.dart b/packages/credential_manifest/lib/src/models/input_descriptor.dart index 19f4b4ab5..99cd5ee81 100644 --- a/packages/credential_manifest/lib/src/models/input_descriptor.dart +++ b/packages/credential_manifest/lib/src/models/input_descriptor.dart @@ -5,13 +5,14 @@ part 'input_descriptor.g.dart'; @JsonSerializable(explicitToJson: true) class InputDescriptor { - InputDescriptor(this.constraints, this.purpose); + InputDescriptor(this.id, this.constraints, this.purpose); factory InputDescriptor.fromJson(Map json) => _$InputDescriptorFromJson(json); final Constraints? constraints; final String? purpose; + final String? id; Map toJson() => _$InputDescriptorToJson(this); } diff --git a/packages/credential_manifest/lib/src/models/presentation_definition.dart b/packages/credential_manifest/lib/src/models/presentation_definition.dart index 4e20c894e..8072b3b95 100644 --- a/packages/credential_manifest/lib/src/models/presentation_definition.dart +++ b/packages/credential_manifest/lib/src/models/presentation_definition.dart @@ -5,10 +5,14 @@ part 'presentation_definition.g.dart'; @JsonSerializable(explicitToJson: true) class PresentationDefinition { - PresentationDefinition(this.inputDescriptors); + PresentationDefinition({ + required this.inputDescriptors, + this.id, + }); factory PresentationDefinition.fromJson(Map json) => _$PresentationDefinitionFromJson(json); + final String? id; @JsonKey(name: 'input_descriptors') final List inputDescriptors;