diff --git a/lib/dashboard/home/tab_bar/credentials/detail/view/credentials_details_page.dart b/lib/dashboard/home/tab_bar/credentials/detail/view/credentials_details_page.dart index 9167427a3..6f5508879 100644 --- a/lib/dashboard/home/tab_bar/credentials/detail/view/credentials_details_page.dart +++ b/lib/dashboard/home/tab_bar/credentials/detail/view/credentials_details_page.dart @@ -321,13 +321,11 @@ class _CredentialsDetailsViewState extends State { /// selective disclouse data - _sd /// and normal data too - if (containClaims) ...[ - DisplaySelectiveDisclosure( - credentialModel: widget.credentialModel, - claims: null, - showVertically: showVerticalDescription, - ), - ], + DisplaySelectiveDisclosure( + credentialModel: widget.credentialModel, + claims: null, + showVertically: showVerticalDescription, + ), // /// normal claims data // if (widget.credentialModel.credentialSupported != diff --git a/lib/dashboard/home/tab_bar/credentials/present/pick/selective_disclosure/cubit/selective_disclosure_pick_cubit.dart b/lib/dashboard/home/tab_bar/credentials/present/pick/selective_disclosure/cubit/selective_disclosure_pick_cubit.dart index 63744fddb..bd120e091 100644 --- a/lib/dashboard/home/tab_bar/credentials/present/pick/selective_disclosure/cubit/selective_disclosure_pick_cubit.dart +++ b/lib/dashboard/home/tab_bar/credentials/present/pick/selective_disclosure/cubit/selective_disclosure_pick_cubit.dart @@ -95,10 +95,11 @@ class SelectiveDisclosureCubit extends Cubit { index = selectiveDisclosure.disclosureFromJWT .indexWhere((entry) => entry == threeDotValue); } else if (claimsKey != null) { - index = - selectiveDisclosure.disclosureToContent.entries.toList().indexWhere( - (entry) => entry.value.toString().contains(claimsKey), - ); + index = selectiveDisclosure.disclosureListToContent.entries + .toList() + .indexWhere( + (entry) => entry.value.toString().contains(claimsKey), + ); } if (index == null || index == -1) { diff --git a/lib/selective_disclosure/selective_disclosure.dart b/lib/selective_disclosure/selective_disclosure.dart index d1aa70c47..f76a591db 100644 --- a/lib/selective_disclosure/selective_disclosure.dart +++ b/lib/selective_disclosure/selective_disclosure.dart @@ -4,6 +4,7 @@ import 'package:altme/app/app.dart'; import 'package:altme/dashboard/home/tab_bar/credentials/models/credential_model/credential_model.dart'; import 'package:altme/selective_disclosure/selective_disclosure.dart'; import 'package:json_path/json_path.dart'; +import 'package:jwt_decode/jwt_decode.dart'; import 'package:oidc4vc/oidc4vc.dart'; export 'model/model.dart'; @@ -11,6 +12,19 @@ class SelectiveDisclosure { SelectiveDisclosure(this.credentialModel); final CredentialModel credentialModel; + Map get payload { + final encryptedValues = credentialModel.jwt + ?.split('~') + .where((element) => element.isNotEmpty) + .toList(); + + final encryptedPayload = encryptedValues!.first; + return decodePayload( + jwtDecode: JWTDecode(), + token: encryptedPayload, + ); + } + Map get claims { final credentialSupported = credentialModel.credentialSupported; @@ -52,7 +66,7 @@ class SelectiveDisclosure { Map get extractedValuesFromJwt { final extractedValues = {}; - for (final element in disclosureToContent.entries.toList()) { + for (final element in disclosureListToContent.entries.toList()) { try { final lisString = jsonDecode(element.value.toString()); if (lisString is List) { @@ -100,23 +114,10 @@ class SelectiveDisclosure { return []; } - Map get disclosureToContent { + Map get disclosureListToContent { final data = {}; - - for (var element in disclosureFromJWT) { - try { - while (element.length % 4 != 0) { - element += '='; - } - - final decryptedData = utf8.decode(base64Decode(element)); - - if (decryptedData.isNotEmpty) { - data[element] = decryptedData; - } - } catch (e) { - // - } + for (final element in disclosureFromJWT) { + data[element] = disclosureToContent(element); } return data; @@ -126,28 +127,42 @@ class SelectiveDisclosure { final data = {}; for (final element in contents) { - try { - final sh256Hash = OIDC4VC().sh256HashOfContent(element); - final lisString = jsonDecode(element); - if (lisString is List) { - if (lisString.length == 3) { - /// '["Qg_O64zqAxe412a108iroA", "phone_number", "+81-80-1234-5678"]' - data[sh256Hash] = {lisString[1]: lisString[2]}; - } else if (lisString.length == 2) { - /// '["Qg_O64zqAxe412a108iroA", "DE'] - data[sh256Hash] = {lisString[0]: lisString[1]}; - } + data.addAll(contentOfSh256Hash(element)); + } + return data; + } + + Map contentOfSh256Hash( + String element, + ) { + final data = {}; + try { + final sh256Hash = OIDC4VC().sh256HashOfContent(element); + // print('element: $element'); + final lisString = jsonDecode(element); + if (lisString is List) { + if (lisString.length == 3) { + /// '["Qg_O64zqAxe412a108iroA", "phone_number", "+81-80-1234-5678"]' + data[sh256Hash] = { + lisString[1]: lisString[2], + }; + } else if (lisString.length == 2) { + /// '["Qg_O64zqAxe412a108iroA", "DE'] + data[sh256Hash] = { + lisString[0]: lisString[1], + }; } - } catch (e) { - // } + return data; + } catch (e) { + // + return data; } - return data; } List get contents { final contents = []; - for (final element in disclosureToContent.entries.toList()) { + for (final element in disclosureListToContent.entries.toList()) { contents.add(element.value.toString()); } return contents; @@ -260,4 +275,19 @@ class SelectiveDisclosure { return value; } + + String disclosureToContent(String element) { + String encryptedData = element; + try { + while (encryptedData.length % 4 != 0) { + encryptedData += '='; + } + + final decryptedData = utf8.decode(base64Decode(encryptedData)); + return decryptedData; + } catch (e) { + return ''; + // + } + } } diff --git a/lib/selective_disclosure/widget/display_selective_disclosure.dart b/lib/selective_disclosure/widget/display_selective_disclosure.dart index 1e2ac71d3..7e7ff561c 100644 --- a/lib/selective_disclosure/widget/display_selective_disclosure.dart +++ b/lib/selective_disclosure/widget/display_selective_disclosure.dart @@ -5,6 +5,8 @@ import 'package:altme/selective_disclosure/selective_disclosure.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:json_path/json_path.dart'; +import 'package:oidc4vc/oidc4vc.dart'; class DisplaySelectiveDisclosure extends StatelessWidget { const DisplaySelectiveDisclosure({ @@ -30,35 +32,154 @@ class DisplaySelectiveDisclosure extends StatelessWidget { final currentClaims = claims ?? selectiveDisclosure.claims; final languageCode = context.read().state.locale.languageCode; + final listOfClaims = + currentClaims.entries.map((MapEntry map) { + String? title; + + final mapKey = map.key; + final mapValue = map.value; + + /// nested day contains more data + if (mapValue is! Map) return Container(); + + final display = getDisplay(mapKey, mapValue, languageCode); + + if (display == null) return Container(); + title = display['name'].toString(); + + final bool hasNestedData = + mapValue.values.any((element) => element is Map); + + if (hasNestedData && parentKeyId == null) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + title, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: DisplaySelectiveDisclosure( + credentialModel: credentialModel, + claims: mapValue, + showVertically: showVertically, + selectiveDisclosureState: selectiveDisclosureState, + parentKeyId: mapKey, + onPressed: (nestedKey, _, threeDotValue) { + onPressed?.call( + nestedKey, + '$mapKey-$nestedKey', + threeDotValue, + ); + }, + ), + ), + ], + ); + } else { + return displayClaimData(mapKey, title, context); + } + }).toList(); + return Column( crossAxisAlignment: CrossAxisAlignment.start, - children: currentClaims.entries.map((MapEntry map) { - String? title; + children: listOfClaims + + filteredListFromJwtEntries( + selectiveDisclosure, + currentClaims, + context, + parentKeyId, + ).toList(), + ); + } - final key = map.key; - final value = map.value; + Iterable filteredListFromJwtEntries( + SelectiveDisclosure selectiveDisclosure, + Map currentClaims, + BuildContext context, + String? parentKeyId, + ) { + if (selectiveDisclosureState == null && + !context.read().state.model.isDeveloperMode) return []; + return selectiveDisclosure.disclosureFromJWT.map((String disclosure) { + /// check if disclosure is in a nested value + /// if so, return empty container + final oidc4vc = OIDC4VC(); + final content = selectiveDisclosure.disclosureToContent(disclosure); - /// nested day contains more data - if (value is! Map) return Container(); + final digest = oidc4vc.sh256HashOfContent( + content, + ); + final disclosuresContent = Map.from( + selectiveDisclosure.disclosureListToContent, + ); - final display = getDisplay(key, value, languageCode); + if (parentKeyId == null) { + disclosuresContent.removeWhere((key, value) { + if (value is String) { + return !value.contains(digest); + } + return true; + }); + if (disclosuresContent.isNotEmpty) return Container(); - if (display == null) return Container(); - title = display['name'].toString(); + /// Check if the disclosure is nested in a element from payload + /// If so, return empty container, else continue building of the widget + if (isNestedInpayload(selectiveDisclosure.contents, digest)) { + return Container(); + } + } else { + + /// keep going only if element is in the nested value of the parentKeyId + /// either in the payload or the claims + + final nestedValue = + selectiveDisclosure.extractedValuesFromJwt[parentKeyId] ?? + SelectiveDisclosure(credentialModel).payload[parentKeyId]; + if (nestedValue == null) return Container(); + bool displayElement = false; + if (nestedValue is String) { + if (nestedValue.contains(digest)) displayElement = true; + } + if (nestedValue is Map) { + final claimList = nestedValue['_sd']; + if (claimList is List) { + if (claimList.contains(digest)) displayElement = true; + } + } + if (!displayElement) return Container(); + } - final bool hasNestedData = - value.values.any((element) => element is Map); + /// Check if the disclosure is already displayed in the claims + /// from the display. If so, return empty container, else build the widget + final value = selectiveDisclosure + .contentOfSh256Hash( + content, + ) + .entries + .first + .value; - if (hasNestedData && parentKeyId == null) { + final element = value.entries.first; + if (!currentClaims.containsKey(element.key)) { + if (element.value is Map) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(top: 10), child: Text( - title, + element.key.toString(), style: Theme.of(context).textTheme.bodyMedium!.copyWith( color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, ), ), ), @@ -66,14 +187,14 @@ class DisplaySelectiveDisclosure extends StatelessWidget { padding: const EdgeInsets.only(left: 10), child: DisplaySelectiveDisclosure( credentialModel: credentialModel, - claims: value, + claims: const {}, showVertically: showVertically, selectiveDisclosureState: selectiveDisclosureState, - parentKeyId: key, + parentKeyId: element.key.toString(), onPressed: (nestedKey, _, threeDotValue) { onPressed?.call( nestedKey, - '$key-$nestedKey', + '${element.key}-$nestedKey', threeDotValue, ); }, @@ -81,126 +202,133 @@ class DisplaySelectiveDisclosure extends StatelessWidget { ), ], ); - } else { - final List claimsData = - SelectiveDisclosure(credentialModel).getClaimsData( - key: key, - ); + } - if (claimsData.isEmpty) return Container(); + final toto = displayClaimData( + element.key.toString(), + element.key.toString(), + context, + ); + return toto; + } else { + return Container(); + } + }); + } - return Column( - children: claimsData.map( - (ClaimsData claims) { - final index = claimsData.indexOf(claims); - var keyToCheck = key; - var claimKey = key; - - if (parentKeyId != null) { - keyToCheck = '$parentKeyId-$key'; - } + Widget displayClaimData(String mapKey, String? title, BuildContext context) { + final List claimsData = + SelectiveDisclosure(credentialModel).getClaimsData( + key: mapKey, + ); - final isFirstElement = index == 0; + if (claimsData.isEmpty) return Container(); - if (!isFirstElement) { - title = null; - keyToCheck = '$keyToCheck-$index'; - claimKey = '$claimKey-$index'; - } + return Column( + children: claimsData.map( + (ClaimsData claims) { + final index = claimsData.indexOf(claims); + var keyToCheck = mapKey; + var claimKey = mapKey; + + if (parentKeyId != null) { + keyToCheck = '$parentKeyId-$mapKey'; + } + + final isFirstElement = index == 0; + + if (!isFirstElement) { + keyToCheck = '$keyToCheck-$index'; + claimKey = '$claimKey-$index'; + } + + final limitDisclosure = selectiveDisclosureState?.limitDisclosure; + + final isCompulsary = + limitDisclosure != null && limitDisclosure == 'required'; - final limitDisclosure = - selectiveDisclosureState?.limitDisclosure; - - final isCompulsary = - limitDisclosure != null && limitDisclosure == 'required'; - - bool isDisabled = isCompulsary; - - final selectedKeyId = selectiveDisclosureState - ?.selectedClaimsKeyIds - .firstWhereOrNull((ele) => ele.keyId == keyToCheck); - - if (selectiveDisclosureState != null) { - final filters = selectiveDisclosureState!.filters; - if (filters != null) { - isDisabled = isCompulsary; - - filters.forEach((key, value) { - if (claims.threeDotValue != null) { - if (claimKey.contains(key) && - claims.data.replaceAll(' ', '') == value) { - if (isCompulsary) isDisabled = false; - - if (selectedKeyId == null) { - onPressed?.call( - key, - claimKey, - claims.threeDotValue, - ); - } - } - } else { - if (claimKey == key && claims.data == value) { - if (isCompulsary) isDisabled = false; - - if (selectedKeyId == null) { - onPressed?.call(key, claimKey, null); - } - } - } - }); + bool isDisabled = isCompulsary; + + final selectedKeyId = selectiveDisclosureState?.selectedClaimsKeyIds + .firstWhereOrNull((ele) => ele.keyId == keyToCheck); + + if (selectiveDisclosureState != null) { + final filters = selectiveDisclosureState!.filters; + if (filters != null) { + isDisabled = isCompulsary; + + filters.forEach((key, value) { + if (claims.threeDotValue != null) { + if (claimKey.contains(key) && + claims.data.replaceAll(' ', '') == value) { + if (isCompulsary) isDisabled = false; + + if (selectedKeyId == null) { + onPressed?.call( + key, + claimKey, + claims.threeDotValue, + ); + } } - } + } else { + if (claimKey == key && claims.data == value) { + if (isCompulsary) isDisabled = false; - return TransparentInkWell( - onTap: () { - if (isDisabled || isCompulsary) { - return; + if (selectedKeyId == null) { + onPressed?.call(key, claimKey, null); } + } + } + }); + } + } - onPressed?.call(key, claimKey, claims.threeDotValue); - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: CredentialField( - padding: - EdgeInsets.only(top: isFirstElement ? 10 : 0), - title: title, - value: claims.data, - titleColor: Theme.of(context).colorScheme.onSurface, - valueColor: Theme.of(context).colorScheme.onSurface, - showVertically: showVertically, - ), - ), - if (selectiveDisclosureState != null && - claims.isfromDisclosureOfJWT) ...[ - const Spacer(), - Padding( - padding: const EdgeInsets.only(top: 0, right: 10), - child: Icon( - (selectedKeyId != null && selectedKeyId.isSelected) - ? Icons.check_box - : Icons.check_box_outline_blank, - size: 25, - color: isDisabled - ? Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.3) - : Theme.of(context).colorScheme.onSurface, - ), - ), - ], - ], + return TransparentInkWell( + onTap: () { + if (isDisabled || isCompulsary) { + return; + } + + onPressed?.call(mapKey, claimKey, claims.threeDotValue); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: CredentialField( + padding: EdgeInsets.only(top: isFirstElement ? 10 : 0), + title: !isFirstElement ? null : title, + value: claims.data, + titleColor: Theme.of(context).colorScheme.onSurface, + valueColor: Theme.of(context).colorScheme.onSurface, + showVertically: showVertically, + ), + ), + if (selectiveDisclosureState != null && + claims.isfromDisclosureOfJWT) ...[ + const Spacer(), + Padding( + padding: const EdgeInsets.only(top: 0, right: 10), + child: Icon( + (selectedKeyId != null && selectedKeyId.isSelected) + ? Icons.check_box + : Icons.check_box_outline_blank, + size: 25, + color: isDisabled + ? Theme.of(context) + .colorScheme + .onSurface + .withOpacity(0.3) + : Theme.of(context).colorScheme.onSurface, + ), ), - ); - }, - ).toList(), + ], + ], + ), ); - } - }).toList(), + }, + ).toList(), ); } @@ -238,4 +366,20 @@ class DisplaySelectiveDisclosure extends StatelessWidget { return null; } } + + bool isNestedInpayload(List contents, String digest) { + final payload = SelectiveDisclosure(credentialModel).payload; + final JsonPath dataPath = JsonPath( + r'$..["_sd"]', + ); + final List list = []; + payload.remove('_sd'); + dataPath.read(payload).forEach((element) { + if (element.value is List) { + list.addAll(element.value! as List); + } + }); + + return list.contains(digest); + } }