From 04132ec64d1e5a123cd847df9d524017955a7707 Mon Sep 17 00:00:00 2001 From: Suzanna Jiwani Date: Thu, 17 Aug 2023 15:27:08 -0400 Subject: [PATCH] Incorporate Interoperability Event Features to wwwverifier Added the ability to chose between request scenarios, as well as required checks + associated logging. Also added the ability to render the returned portrait image. Manually tested. --- .../mdoc/engagement/EngagementParser.java | 10 +- .../mdoc/origininfo/OriginInfoBaseUrl.java | 2 +- .../origininfo/OriginInfoReferrerUrl.java | 2 +- wwwverifier/build.gradle | 2 + .../identity/wwwreader/RequestServlet.java | 326 +++++++++++++++--- .../identity/wwwreader/ServletConsts.java | 5 + wwwverifier/src/main/webapp/index.html | 18 +- wwwverifier/src/main/webapp/script.js | 60 +++- wwwverifier/src/main/webapp/style.css | 14 +- .../wwwreader/RequestServletTest.java | 3 +- 10 files changed, 375 insertions(+), 67 deletions(-) diff --git a/identity/src/main/java/com/android/identity/mdoc/engagement/EngagementParser.java b/identity/src/main/java/com/android/identity/mdoc/engagement/EngagementParser.java index 2cd07627f..4eb977f40 100644 --- a/identity/src/main/java/com/android/identity/mdoc/engagement/EngagementParser.java +++ b/identity/src/main/java/com/android/identity/mdoc/engagement/EngagementParser.java @@ -172,9 +172,13 @@ void parse(@NonNull byte[] encodedEngagement) { if (Util.cborMapHasKey(map, 5)) { List originInfoItems = Util.cborMapExtractArray(map, 5); for (DataItem oiDataItem : originInfoItems) { - OriginInfo originInfo = OriginInfo.decode(oiDataItem); - if (originInfo != null) { - mOriginInfos.add(originInfo); + try { + OriginInfo originInfo = OriginInfo.decode(oiDataItem); + if (originInfo != null) { + mOriginInfos.add(originInfo); + } + } catch (Exception e) { + Logger.w(TAG, "OriginInfo is incorrectly formatted.", e); } } } diff --git a/identity/src/main/java/com/android/identity/mdoc/origininfo/OriginInfoBaseUrl.java b/identity/src/main/java/com/android/identity/mdoc/origininfo/OriginInfoBaseUrl.java index 8e243311b..b54bc4373 100644 --- a/identity/src/main/java/com/android/identity/mdoc/origininfo/OriginInfoBaseUrl.java +++ b/identity/src/main/java/com/android/identity/mdoc/origininfo/OriginInfoBaseUrl.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/identity/src/main/java/com/android/identity/mdoc/origininfo/OriginInfoReferrerUrl.java b/identity/src/main/java/com/android/identity/mdoc/origininfo/OriginInfoReferrerUrl.java index 9e997b8d1..43a746ed9 100644 --- a/identity/src/main/java/com/android/identity/mdoc/origininfo/OriginInfoReferrerUrl.java +++ b/identity/src/main/java/com/android/identity/mdoc/origininfo/OriginInfoReferrerUrl.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 The Android Open Source Project + * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/wwwverifier/build.gradle b/wwwverifier/build.gradle index 054ab3852..d3d09d8b9 100644 --- a/wwwverifier/build.gradle +++ b/wwwverifier/build.gradle @@ -28,6 +28,8 @@ dependencies { implementation 'co.nstant.in:cbor:0.9' implementation 'org.json:json:20160810' implementation 'org.bouncycastle:bcprov-jdk15on:1.70' + implementation 'com.google.cloud:google-cloud-storage' + implementation platform('com.google.cloud:libraries-bom:26.22.0') implementation project(path: ':identity') testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:3.2.4' diff --git a/wwwverifier/src/main/java/com/android/identity/wwwreader/RequestServlet.java b/wwwverifier/src/main/java/com/android/identity/wwwreader/RequestServlet.java index 130e35d22..33f065a86 100644 --- a/wwwverifier/src/main/java/com/android/identity/wwwreader/RequestServlet.java +++ b/wwwverifier/src/main/java/com/android/identity/wwwreader/RequestServlet.java @@ -28,10 +28,13 @@ import com.android.identity.mdoc.request.DeviceRequestGenerator; import com.android.identity.mdoc.response.DeviceResponseParser; import com.android.identity.mdoc.sessionencryption.SessionEncryption; +import com.android.identity.util.CborUtil; import com.android.identity.util.Timestamp; import com.google.gson.Gson; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -70,6 +73,13 @@ import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.Text; +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + /** * This servlet performs three main functions: * (1) Generates a mdoc:// URI upon button click, containing a ReaderEngagement CBOR message @@ -82,27 +92,99 @@ public class RequestServlet extends HttpServlet { public static final DatastoreService ds = DatastoreServiceFactory.getDatastoreService(); + + private static boolean mStoreLogs = false; + private static final String bucketName = "mdoc-reader-external.appspot.com"; // The ID of your GCS bucket /** * Handles two types of HTTP GET requests: * (1) A request to create a new session, which involves creating a new entity in Datastore * (2) A request to retrieve information from DeviceResponse from an existing session + * (3) A request to retrieve the logs from an existing session in order to be displayed * - * @return String, containing either (1) a generated mdoc:// URI and unique Datastore key - * or (2) parsed DeviceResponse data + *

Adds the requested information to the given response as a String, containing either + * (1) a generated mdoc:// URI and unique Datastore key or + * (2) parsed DeviceResponse data or + * (3) formatted logs */ @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;"); String[] pathArr = parsePathInfo(request); + if (pathArr[0].equals(ServletConsts.SESSION_URL)) { - response.getWriter().println(createNewSession()); + Map urlQueryParams = request.getParameterMap(); + String[] requestedAttributes = urlQueryParams.getOrDefault( + ServletConsts.REQUESTED_ATTRIBUTES_QUERY, new String[]{"all"}); + if (requestedAttributes.length != 1) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + response.getWriter().println(createNewSession(requestedAttributes[0])); + } else if (pathArr[0].equals(ServletConsts.RESPONSE_URL) && pathArr.length == 2) { Key key = com.google.appengine.api.datastore.KeyFactory.stringToKey(pathArr[1]); response.getWriter().println(getDeviceResponse(key)); + + } else if (pathArr[0].equals(ServletConsts.DISPLAY_LOGS_URL) && pathArr.length == 2) { + Key key = com.google.appengine.api.datastore.KeyFactory.stringToKey(pathArr[1]); + response.getWriter().println(getLogs(key)); + } + } + + /** + * Handles two distinct POST requests: + * (1) The first POST request sent to the servlet, which contains a MessageData message + * (2) The second POST request sent to the servlet, which contains a DeviceResponse message + * + * @param request encoded CBOR message + * @return (1) response, containing a DeviceRequest message as an encoded byte array; + * (2) response, containing a termination message with status code 20 + */ + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + String[] pathArr = parsePathInfo(request); + Key key = com.google.appengine.api.datastore.KeyFactory.stringToKey(pathArr[0]); + if (getNumPostRequests(key) == 0) { + byte[] sessionData; + try { + sessionData = createDeviceRequest(getBytesFromRequest(request), key); + } catch (Exception e) { + log(key, String.format("[%s] Check: Provide \"RestApi\" request: Failed\n", + millisToSec(System.currentTimeMillis()))); + log(key, String.format("[%s] Comment: %s\n", millisToSec(System.currentTimeMillis()), e)); + throw e; + } + log(key, String.format("[%s] Check: Provide \"RestApi\" request: OK\n", + millisToSec(System.currentTimeMillis()))); + setNumPostRequests(1, key); + response.getOutputStream().write(sessionData); + + } else if (getNumPostRequests(key) == 1) { + log(key, String.format("[%s] Check: Receive \"RestApi\" response: OK\n", + millisToSec(System.currentTimeMillis()))); + byte[] terminationMessage = parseDeviceResponse(getBytesFromRequest(request), key); + setNumPostRequests(2, key); + response.getOutputStream().write(terminationMessage); + + } else { + log(key, String.format("[%s] Check: Receive \"RestApi\" response: FAILED\n", + millisToSec(System.currentTimeMillis()))); + log(key, String.format("[%s] Comment: Received more than one POST request\n", + millisToSec(System.currentTimeMillis()))); + response.sendError(HttpServletResponse.SC_BAD_REQUEST); } } + /** + * Set whether the logs generated should be stored in cloud storage + * + * @param storeLogs whether logs should be stored + */ + public void setStoreLogs(boolean storeLogs) { + mStoreLogs = storeLogs; + } + /** * @param request Either a GET or POST request * @return a String array containing the parsed path information, which includes at least @@ -116,19 +198,38 @@ public static String[] parsePathInfo(HttpServletRequest request) { /** * Creates a new Entity in Datastore for the new session and generates ReaderEngagement. - * + * + * @param requestedAttributes either "SCE_REST_1" or "SCE_REST_2", in order to indicate what + * attributes should be included in the request * @return String containing the generated mdoc URL and the unique key tied to the * new session's entry in Datastore, separated with a comma */ - public static String createNewSession() { + public static String createNewSession(String requestedAttributes) { Entity entity = new Entity(ServletConsts.ENTITY_TYPE); entity.setProperty(ServletConsts.TIMESTAMP_PROP, new java.sql.Timestamp(System.currentTimeMillis()).toString()); + entity.setUnindexedProperty(ServletConsts.REQ_ATTR_PROP, requestedAttributes); ds.put(entity); Key key = entity.getKey(); + String keyStr = com.google.appengine.api.datastore.KeyFactory.keyToString(key); setNumPostRequests(0, key); - return createMdocUri(key) + ServletConsts.SESSION_SEPARATOR + keyStr; + String toReturn = createMdocUri(key) + ServletConsts.SESSION_SEPARATOR + keyStr; + + log(key, "Verifier: RO-16\n"); + log(key, "Protocol: RestApi\n"); + log(key, "Transaction: " + key.getId() + "\n"); + log(key, "Started: " + millisToSec(System.currentTimeMillis()) + "\n"); + String scenario = requestedAttributes.equals("all")? "SCE_REST_1" : "SCE_REST_2"; + log(key, "Scenario: " + scenario + "\n"); + + return toReturn; + } + + private static String millisToSec(long timeMillis) { + String millisString = Long.toString(timeMillis); + int len = millisString.length(); + return millisString.substring(0, len-3) + "." + millisString.substring(len-3, len); } /** @@ -151,32 +252,6 @@ private static String createMdocUri(Key key) { return ServletConsts.MDOC_PREFIX + reStr; } - /** - * Handles two distinct POST requests: - * (1) The first POST request sent to the servlet, which contains a MessageData message - * (2) The second POST request sent to the servlet, which contains a DeviceResponse message - * - * @param request encoded CBOR message - * @return (1) response, containing a DeviceRequest message as an encoded byte array; - * (2) response, containing a termination message with status code 20 - */ - @Override - public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { - String[] pathArr = parsePathInfo(request); - Key key = com.google.appengine.api.datastore.KeyFactory.stringToKey(pathArr[0]); - if (getNumPostRequests(key) == 0) { - byte[] sessionData = createDeviceRequest(getBytesFromRequest(request), key); - setNumPostRequests(1, key); - response.getOutputStream().write(sessionData); - } else if (getNumPostRequests(key) == 1) { - byte[] terminationMessage = parseDeviceResponse(getBytesFromRequest(request), key); - setNumPostRequests(2, key); - response.getOutputStream().write(terminationMessage); - } else { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - } - } - /** * Parses the MessageData CBOR message to isolate DeviceEngagement. DeviceEngagement * is then used to create a DeviceRequest CBOR message. @@ -188,6 +263,8 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr private static byte[] createDeviceRequest(byte[] messageData, Key key) { byte[] encodedEngagement = Util.cborMapExtractByteString(Util.cborDecode(messageData), ServletConsts.DE_KEY); + log(key, String.format("[%s] Comment: Received Engagement CBOR %s\n", + millisToSec(System.currentTimeMillis()), cborPrettyPrint(encodedEngagement))); PublicKey eReaderKeyPublic = getPublicKey(getDatastoreProp(ServletConsts.PUBKEY_PROP, key)); PrivateKey eReaderKeyPrivate = getPrivateKey(getDatastoreProp(ServletConsts.PRIVKEY_PROP, key)); @@ -204,15 +281,16 @@ private static byte[] createDeviceRequest(byte[] messageData, Key key) { ser.setSendSessionEstablishment(false); byte[] dr = new DeviceRequestGenerator() .setSessionTranscript(sessionTranscript) - .addDocumentRequest(ServletConsts.MDL_DOCTYPE, createMdlItemsToRequest(), null, null, null) + .addDocumentRequest(ServletConsts.MDL_DOCTYPE, createMdlItemsToRequest(key), null, null, null) .generate(); return ser.encryptMessage(dr, OptionalLong.empty()); } /** - * Generates session transcript using the device engagement @param de , the - * reader key @param eReaderKeyPublic , and the reader engagement, which is accessed via - * the datastore key @param key . + * Generates session transcript using the device engagement + * @param de the reader key + * @param eReaderKeyPublic the reader engagement + * @param key the datastore key */ public static byte[] buildSessionTranscript(byte[] de, PublicKey eReaderKeyPublic, Key key) { byte[] re = getDatastoreProp(ServletConsts.RE_PROP, key); @@ -225,8 +303,10 @@ public static byte[] buildSessionTranscript(byte[] de, PublicKey eReaderKeyPubli } /** - * Verify that the base URL of the origin info found in @param oiList matches + * Verify that the base URL of the origin info found in the given origin infos matches * the base URL of the website (ServletConsts.BASE_URL). + * + * @param oiList list of origin info objects * @param key Unique identifier corresponding to the current session */ private static void verifyOriginInfo(List oiList, Key key) { @@ -235,19 +315,35 @@ private static void verifyOriginInfo(List oiList, Key key) { if (!oiUrl.equals(ServletConsts.BASE_URL)) { setOriginInfoStatus(ServletConsts.OI_FAILURE_START + oiUrl + ServletConsts.OI_FAILURE_END, key); + log(key, String.format("[%s] Comment: OriginInfo URL %s\n", + millisToSec(System.currentTimeMillis()), oiUrl)); } else { setOriginInfoStatus(ServletConsts.OI_SUCCESS, key); } } else { setOriginInfoStatus(ServletConsts.OI_FAILURE_START + ServletConsts.OI_FAILURE_END.trim(), key); + log(key, String.format("[%s] Comment: OriginInfo is incorrectly formatted. " + + "We expect OriginInfo = {\"cat\": uint, \"type\": uint, \"details\": tstr} in " + + "accordance with the recent CD consultation of 18013-7.\n", + millisToSec(System.currentTimeMillis()))); } } /** * @return Map of items to request from the mDL app */ - private static Map> createMdlItemsToRequest() { + private static Map> createMdlItemsToRequest(Key key) { + Entity entity = getEntity(key); + String requestedAttributes = (String) entity.getProperty(ServletConsts.REQ_ATTR_PROP); + if (requestedAttributes.equals("age")){ + return createMdlItemsToRequestAge18(); + } + return createMdlItemsToRequestFull(); + + } + + private static Map> createMdlItemsToRequestFull() { Map> mdlItemsToRequest = new HashMap<>(); Map mdlNsItems = new HashMap<>(); mdlNsItems.put("sex", false); @@ -258,11 +354,25 @@ private static Map> createMdlItemsToRequest() { mdlNsItems.put("family_name", false); mdlNsItems.put("document_number", false); mdlNsItems.put("issuing_authority", false); + mdlNsItems.put("birth_date", false); + mdlNsItems.put("issuing_country", false); + mdlNsItems.put("driving_privileges", false); + mdlNsItems.put("un_distinguishing_sign", false); mdlItemsToRequest.put(ServletConsts.MDL_NAMESPACE, mdlNsItems); + Map aamvaNsItems = new HashMap<>(); aamvaNsItems.put("DHS_compliance", false); aamvaNsItems.put("EDL_credential", false); mdlItemsToRequest.put(ServletConsts.AAMVA_NAMESPACE, aamvaNsItems); + + return mdlItemsToRequest; + } + + private static Map> createMdlItemsToRequestAge18() { + Map> mdlItemsToRequest = new HashMap<>(); + Map mdlNsItems = new HashMap<>(); + mdlNsItems.put("age_over_18", false); + mdlItemsToRequest.put(ServletConsts.MDL_NAMESPACE, mdlNsItems); return mdlItemsToRequest; } @@ -316,7 +426,7 @@ private static byte[] getBytesFromRequest(HttpServletRequest request) { * @return byte array containing an empty SessionData message with status code 20 * to indicate termination. */ - private static byte[] parseDeviceResponse(byte[] messageData, Key key) { + private static byte[] parseDeviceResponse(byte[] messageData, Key key) throws IOException { PublicKey eReaderKeyPublic = getPublicKey(getDatastoreProp(ServletConsts.PUBKEY_PROP, key)); PrivateKey eReaderKeyPrivate = @@ -325,15 +435,26 @@ private static byte[] parseDeviceResponse(byte[] messageData, Key key) { getPublicKey(getDatastoreProp(ServletConsts.DEVKEY_PROP, key)); byte[] sessionTranscript = getDatastoreProp(ServletConsts.TRANSCRIPT_PROP, key); - SessionEncryption ser = new SessionEncryption(SessionEncryption.ROLE_MDOC_READER, - new KeyPair(eReaderKeyPublic, eReaderKeyPrivate), eDeviceKeyPublic, sessionTranscript); - ser.setSendSessionEstablishment(false); - - DeviceResponseParser.DeviceResponse dr = new DeviceResponseParser() - .setDeviceResponse(ser.decryptMessage(messageData).getData()) - .setSessionTranscript(sessionTranscript) - .setEphemeralReaderKey(eReaderKeyPrivate) - .parse(); + SessionEncryption ser; + DeviceResponseParser.DeviceResponse dr; + try { + ser = new SessionEncryption(SessionEncryption.ROLE_MDOC_READER, + new KeyPair(eReaderKeyPublic, eReaderKeyPrivate), eDeviceKeyPublic, sessionTranscript); + ser.setSendSessionEstablishment(false); + + dr = new DeviceResponseParser() + .setDeviceResponse(ser.decryptMessage(messageData).getData()) + .setSessionTranscript(sessionTranscript) + .setEphemeralReaderKey(eReaderKeyPrivate) + .parse(); + } catch (Exception e) { + log(key, String.format("[%s] Check: Decrypt response: FAILED\n", + millisToSec(System.currentTimeMillis()))); + log(key, String.format("[%s] Comment: %s\n", millisToSec(System.currentTimeMillis()), e)); + throw new IOException(e); + } + log(key, String.format("[%s] Check: Decrypt response: OK\n", + millisToSec(System.currentTimeMillis()))); String json = new Gson().toJson(buildArrayFromDocuments(dr.getDocuments(), key)); setDeviceResponse(json, key); @@ -350,7 +471,9 @@ private static byte[] parseDeviceResponse(byte[] messageData, Key key) { * on the website */ private static ArrayList buildArrayFromDocuments(List docs, Key key) { - ArrayList arr = new ArrayList(); + ArrayList arr = new ArrayList<>(); + List entriesForVData3Check = new ArrayList<>(); + arr.add(getOriginInfoStatus(key)); arr.add("Number of documents returned: " + docs.size()); for (DeviceResponseParser.Document doc : docs) { @@ -358,11 +481,13 @@ private static ArrayList buildArrayFromDocuments(List buildArrayFromDocuments(List buildArrayFromDocuments(List requestedAll = Arrays.asList("sex", "portrait", "given_name", "issue_date", + "expiry_date", "family_name", "document_number", "issuing_authority", "birth_date", + "issuing_country", "driving_privileges", "un_distinguishing_sign", "DHS_compliance", + "EDL_credential"); + List expected = requestedAttributes.equals("age")? + Collections.singletonList("age_over_18") : requestedAll; + + boolean vData3Check; + if (requestedAttributes.equals("age")){ + vData3Check = entriesForVData3Check.contains("age_over_18"); + vData3Check &= entriesForVData3Check.size() == 1; + } else { + vData3Check = entriesForVData3Check.containsAll(requestedAll); + vData3Check &= entriesForVData3Check.size() == requestedAll.size(); + } + + if (!vData3Check) { + log(key, String.format("[%s] Check: Receive expected data set: FAILED\n", + millisToSec(System.currentTimeMillis()))); + log(key, String.format("[%s] Comment: Received %s, Expected %s\n", + millisToSec(System.currentTimeMillis()), entriesForVData3Check, expected)); + } else { + log(key, String.format("[%s] Check: Receive expected data set: OK\n", + millisToSec(System.currentTimeMillis()))); + } return arr; } @@ -411,7 +574,7 @@ private static ArrayList buildArrayFromDocuments(List +

Android Identity Credential MDoc Reader

+
+ + + +
+
-

Android Identity Credential MDoc Reader


+
+ +
+
+
+
\ No newline at end of file diff --git a/wwwverifier/src/main/webapp/script.js b/wwwverifier/src/main/webapp/script.js index 411649d88..dc9a5c4cc 100644 --- a/wwwverifier/src/main/webapp/script.js +++ b/wwwverifier/src/main/webapp/script.js @@ -1,9 +1,12 @@ -const GET_URL = '/request-mdl'; +const BASE_URL = '/request-mdl'; const CREATE_SESSION_URL = '/create-new-session'; const DISPLAY_RESPONSE_URL = '/display-response'; +const DISPLAY_LOGS_URL = '/display-logs'; const MDOC_URI_ID = 'mdoc-uri-text' const RESPONSE_ID = 'response-display'; +const LOGS_ID = 'logs' +const PORTRAIT_ID = 'portrait-render' const INTERVAL_MS = 5000; // 5 seconds @@ -13,13 +16,51 @@ const CROSS_PLACEHOLDER = '+'; const CROSS_UNICODE = '\u274c'; const BOLD_PLACEHOLDER = '#'; +const AGE_ONLY_REQUEST = 'age'; +const ALL_LICENSE_HOLDER_REQUEST = 'all'; + var sessionID = ''; -window.onload = onLoad; var devResponseInterval = window.setInterval(getDeviceResponse, INTERVAL_MS); +var readLogsInterval = window.setInterval(readLogs, INTERVAL_MS); +var logTextArr = ''; + +function readLogs() { + if (sessionID.length == 0) return; + + + fetch(BASE_URL + DISPLAY_LOGS_URL + '/' + String(sessionID)).then(response => response.text()).then((responseText) => { + if (responseText.length > 1) { + var textArr = responseText.substring(0, responseText.length - 2).split('\n'); + if (responseText === logTextArr) return; + logTextArr = responseText; + + document.getElementById(LOGS_ID).innerHTML = ''; -function onLoad() { - fetch(GET_URL + CREATE_SESSION_URL).then(response => response.text()).then((responseText) => { + // create text instruction + var log_caption = document.createElement('div'); + log_caption.innerHTML = 'Logs for Interop Event'; + document.getElementById(LOGS_ID).append(document.createElement('br')); + document.getElementById(LOGS_ID).appendChild(log_caption); + + var table = document.createElement('table'); + + for (var i = 0; i < textArr.length; i++) { + var row = table.insertRow(i); + var rowText = textArr[i] + row.insertCell(0).innerHTML = rowText; + } + document.getElementById(LOGS_ID).append(table); + } + }); +} + +function createSession(requestedAttributes) { + fetch(BASE_URL + CREATE_SESSION_URL + '?' + new URLSearchParams({ requested_attributes: requestedAttributes }).toString(), + { + method: "get", + }) + .then(response => response.text()).then((responseText) => { var responseArr = responseText.split(','); var mdocURL = responseArr[0]; sessionID = responseArr[1]; @@ -36,13 +77,14 @@ function onLoad() { a.innerHTML = mdocURL; document.getElementById(MDOC_URI_ID).appendChild(a); }); + document.getElementById('buttons').style.display = 'none'; } function getDeviceResponse() { if (sessionID.length == 0) return; document.getElementById(RESPONSE_ID).innerHTML = ''; - fetch(GET_URL + DISPLAY_RESPONSE_URL + '/' + String(sessionID)).then(response => response.text()).then((responseText) => { + fetch(BASE_URL + DISPLAY_RESPONSE_URL + '/' + String(sessionID)).then(response => response.text()).then((responseText) => { if (responseText.length > 1) { var table = document.createElement('table'); var textArr = responseText.substring(1, responseText.length - 2).split(','); @@ -50,6 +92,12 @@ function getDeviceResponse() { var row = table.insertRow(i); var rowText = textArr[i].substring(1, textArr[i].length - 1).split(':'); var rowKey = rowText[0]; + if (rowKey === 'portraitBytes') { + var rowVal = rowText[1].trim(); + document.getElementById(PORTRAIT_ID).src = "data:image/jpeg;base64," + rowVal; + continue; + } + if (rowKey.charAt(0) == CHECKMARK_PLACEHOLDER) { rowKey = CHECKMARK_UNICODE + rowKey.substring(1); } else if (rowKey.charAt(0) == CROSS_PLACEHOLDER) { @@ -59,6 +107,7 @@ function getDeviceResponse() { rowKey = rowKey.substring(1).bold(); } row.insertCell(0).innerHTML = rowKey; + if (rowText.length == 2) { var rowVal = rowText[1].trim(); if (rowVal.charAt(0) == BOLD_PLACEHOLDER) { @@ -66,6 +115,7 @@ function getDeviceResponse() { } row.insertCell(1).innerHTML = rowVal; } + } document.getElementById(RESPONSE_ID).append(table); window.clearInterval(devResponseInterval); diff --git a/wwwverifier/src/main/webapp/style.css b/wwwverifier/src/main/webapp/style.css index 8d67bccb1..eabd05a04 100644 --- a/wwwverifier/src/main/webapp/style.css +++ b/wwwverifier/src/main/webapp/style.css @@ -1,9 +1,15 @@ #content { - margin-left: auto; - margin-right: auto; - width: 650px; + display: flex; + justify-content: flex-start; + flex-wrap: wrap; } #mdoc-uri-text, #response-display { - overflow-wrap:break-word; + overflow-wrap:break-word; +} + +#buttons { + display: flex; + justify-content: flex-start; + flex-wrap: wrap; } \ No newline at end of file diff --git a/wwwverifier/src/test/java/com/android/identity/wwwreader/RequestServletTest.java b/wwwverifier/src/test/java/com/android/identity/wwwreader/RequestServletTest.java index 6baa55b61..3219191f0 100644 --- a/wwwverifier/src/test/java/com/android/identity/wwwreader/RequestServletTest.java +++ b/wwwverifier/src/test/java/com/android/identity/wwwreader/RequestServletTest.java @@ -105,6 +105,7 @@ public class RequestServletTest { public void setUp() throws IOException { MockitoAnnotations.initMocks(this); servlet = new RequestServlet(); + servlet.setStoreLogs(false); helper.setUp(); } @@ -388,7 +389,7 @@ private void sendGetNewSessionRequest() throws IOException { * Creates a new session, and returns a unique identifier associated with it */ private String createSessionKey() { - return RequestServlet.createNewSession().split(",")[1]; + return RequestServlet.createNewSession("all").split(",")[1]; } /**