diff --git a/README.md b/README.md index f131f9e..f8e223a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # initial controller (java) -initial 플랫폼 서비스를 사용하기 위한 Java기반의 Issuer, Verifier, Holder controller 코드를 제공합니다. +initial 플랫폼 서비스를 사용하기 위한 Java기반의 Issuer, Verifier controller 와 Holder 코드를 제공합니다. ## Steps to run +용도에 따라 Issuer 또는 Verifier 를 선택하여 생성하고 테스트 바랍니다. + ### 1. 사전준비 - 이니셜 웹 콘솔 테스트넷: https://dev-console.myinitial.io @@ -10,7 +12,7 @@ initial 플랫폼 서비스를 사용하기 위한 Java기반의 Issuer, Verifie - 기관명: Issuer명 (변경되지 앖는 값, 신중하게 작성하세요) - 도메인 접속 URL: https://issuer-controller.url (예시) - Webhook URL: https://issuer-controller.url/webhooks (webhooks 변경하지 마세요) -- 도메인 접속 URL: https://issuer-controller.url/invitation-url (invitation-url 변경하지 마세요) +- Invitation URL: https://issuer-controller.url/invitation-url (invitation-url 변경하지 마세요) - 기관 구분: Issuer, Verifier 동시 선택 - AppType: Android, iOS 동시 선택 - App 노출: 미사용 (협의 후 사용) @@ -18,12 +20,12 @@ initial 플랫폼 서비스를 사용하기 위한 Java기반의 Issuer, Verifie (아래는 샘플 데모를 수행하기 위함으로, 추후 새로 작성하여 사용하세요) -검증관리 - 검증 양식 생성 - 참여기관별 증명 양식 - (샘플)이니셜모바일가입증명 - 샘플 모바일 가입증명 - 생성하기 -- 검증 양식명: 샘플 모바일 가입 증명 검증 (예시) -- 양식 설명: 샘플 모바일 가입 증명 검증 (예시) +검증관리 - 검증 양식 생성 - 참여기관별 증명 양식 - 증명서양식 기반 - 이니셜 모바일가입증명 - 생성하기 +- 검증 양식명: 모바일 가입 증명 검증 (예시) +- 양식 설명: 모바일 가입 증명 검증 (예시) - 검증 항목 선택: person_name, mobile_num 등 -검증관리 - 검증 양식 관리 - 샘플 모바일 가입 증명 검증 - 상세 보기 +검증관리 - 검증 양식 관리 - 모바일 가입 증명 검증 - 상세 보기 - 검증 양식 ID (`verifTplId`): `0012d683-bdb0-4050-ac85-ae37e59bad09` (예시) **확인** 발행관리 - 발행 양식 생성 - 샘플 학위증명(1.0) @@ -39,40 +41,49 @@ initial 플랫폼 서비스를 사용하기 위한 Java기반의 Issuer, Verifie - `Access Token`: `514ac4f8-e0da-43c9-910d-4894279909b2` (예시) **확인** - Webhook URL: https://issuer-controller.url/webhooks (**서버 주소 및 webhooks 다시 한번 확인**) -#### Holder 생성 및 설정 (Issuer 동작 확인 위함) +#### Verifier 생성 및 설정 기관 생성하기 -- 기관명: Holder명 (테스트 용도) -- 도메인 접속 URL: https://holder-controller.url (예시) -- Webhook URL: https://holder-controller.url/webhooks (webhooks 변경하지 마세요) -- 도메인 접속 URL: https://holder-controller.url/invitation-url (invitation-url 변경하지 마세요) +- 기관명: Verifier명 (변경되지 앖는 값, 신중하게 작성하세요) +- 도메인 접속 URL: https://verifier-controller.url (예시) +- Webhook URL: https://verifier-controller.url/webhooks (webhooks 변경하지 마세요) +- Invitation URL: https://verifier-controller.url/invitation-url (invitation-url 변경하지 마세요) - 기관 구분: Verifier 선택 - AppType: Android, iOS 동시 선택 -- App 노출: 미사용 -- 기관 사용: 미사용 +- App 노출: 미사용 (협의 후 사용) +- 기관 사용: 미사용 (협의 후 사용) + +(아래는 샘플 데모를 수행하기 위함으로, 추후 새로 작성하여 사용하세요) + +검증관리 - 검증 양식 생성 - 참여기관별 증명 양식 - 증명서양식 기반 - 이니셜 모바일가입증명 - 생성하기 +- 검증 양식명: 모바일 가입 증명 검증 (예시) +- 양식 설명: 모바일 가입 증명 검증 (예시) +- 검증 항목 선택: person_name, mobile_num 등 + +검증관리 - 검증 양식 관리 - 샘플 모바일 가입 증명 검증 - 상세 보기 +- 검증 양식 ID (`verifTplId`): `0012d683-bdb0-4050-ac85-ae37e59bad09` (예시) **확인** 기관관리 - 기관 정보 -- `Access Token`: `3a0ece13-dd04-419d-b3ea-f12b52e297d7` (예시) **확인** -- Webhook URL: https://holder-controller.url/webhooks (**서버 주소 및 webhooks 다시 한번 확인**) +- `Access Token`: `514ac4f8-e0da-43c9-910d-4894279909b2` (예시) **확인** +- Webhook URL: https://verifier-controller.url/webhooks (**서버 주소 및 webhooks 다시 한번 확인**) ### 2. properties 설정 - 본 repository 코드 `src/main/resources/` #### application-issuer.properties server.port = 8040 \ -agentApiUrl = https://dev-console.myinitial.io/agent/api (고정) \ +agentApiUrl = https://dev-console.myinitial.io/agent/api 혹은 https://dev-console.myinitial.io/agent/v2/api (기관 설정 값에서 확인) \ accessToken = issuer의 `Access Token` \ credDefId = 작성한 issuer의 `CredDefId` \ verifTplId = 작성한 issuer의 `verifTplId` \ webViewUrl = `https://issuer-controller.url/web-view/form.html` (Optional) Holder 에게 보여줄 Web View 페이지 주소 -#### application-holder.properties -server.port = 8041 \ -agentApiUrl = https://dev-console.myinitial.io/agent/api (고정) \ -accessToken = holder의 `Access Token` \ -issuerInvitationUrl = `https://issuer-controller.url/invitation-url` invitation-url 을 받을 주소 \ -issuerCredDefId = 작성한 issuer의 `CredDefId` +#### application-verifier.properties +server.port = 8040 \ +agentApiUrl = https://dev-console.myinitial.io/agent/api 혹은 https://dev-console.myinitial.io/agent/v2/api (기관 설정 값에서 확인) \ +accessToken = verifier `Access Token` \ +verifTplId = 작성한 issuer의 `verifTplId` -### 3. issuer 및 holder 실행 - 각 terminal +### 3. issuer 또는 verifier 실행 #### issuer 실행 (issuer terminal) ``` ./gradlew issuer @@ -80,25 +91,54 @@ issuerCredDefId = 작성한 issuer의 `CredDefId` 또는 web view 로직이 들어간 데모를 수행하려는 경우 `./gradlew issuer_webview` \ 또는 revocation 로직이 들어간 데모를 수행하려는 경우 `./gradlew issuer_revoke` -#### 정상 구동 시 메시지 (issuer terminal) +#### verifier 실행 (verifier terminal) +``` +./gradlew verifier +``` + +#### 정상 구동 시 메시지 (issuer 또는 verifier terminal) ``` [GlobalService.java]initializeAfterStartup(61) : Controller is ready ``` +### 4. holder 실행 (issuer 또는 verifier 테스트 위함) + +#### holder 설정 변경 +`src/main/java/com/sktelecom/initial/controller/holder/Application.java` + +String appMode = "dev"; // dev 또는 prod \ +String runType = "issue"; // issue 또는 verify + +String tpIssuerInvitationUrl = "https://issuer-controller.url/invitation-url"; \ +String tpCredDefId = "작성한 issuer의 `CredDefId`"; + +또는 + +String tpVerifierInvitationUrl = "https://verifier-controller.url/invitation-url"; + +초기값은 dev환경의 미리 설정되어 있는 test issuer 또는 test verifier 로 설정되어 있습니다. \ +변경없이 우선 테스트 해보시면 holder 동작을 파악하실 수 있습니다. + #### holder 실행 (holder terminal) ``` ./gradlew holder ``` -#### issuer 의 증명서가 정상 발급 된 경우 메시지 (holder terminal) +#### issuer 서비스 사용하여 증명서가 정상 발급 된 경우 메시지 (issuer terminal) +``` +2021-08-10 17:25:48 [INFO ] [GlobalService.java]handleEvent(97) : - Case (topic:issue_credential, state:credential_acked) -> credential issued successfully +``` + +#### verifier 서비스 사용하여 정상 검증된 경우 메시지 (verifier terminal) ``` -[GlobalService.java]handleEvent(97) : - Case (topic:issue_credential, state:credential_acked) -> credential received successfully +2021-08-10 17:24:38 [INFO ] [GlobalService.java]handleEvent(77) : - Case (topic:present_proof, state:verified) -> getPresentationResult +2021-08-10 17:24:38 [INFO ] [GlobalService.java]getPresentationResult(283) : Requested Attribute - person_name: 김증명 +2021-08-10 17:24:38 [INFO ] [GlobalService.java]getPresentationResult(283) : Requested Attribute - mobile_num: 01023456789 ``` -## Work flow +## Issuer Work flow ### Initialization -Issuer는 accessToken, credDefId, verifTplId 가 valid 한 지 확인 하고 대기함. \ -Holder는 accessToken이 valid 한 지 확인 후, 샘플 모바일 가입증명을 발급 받고, 아래 과정 진행. +Issuer는 accessToken, credDefId, verifTplId, webhookUrl 이 valid 한 지 확인 하고 대기함. ### Connection Holder가 https://issuer-controller.url/invitation-url 호출부터 시작 @@ -111,14 +151,12 @@ Holder가 https://issuer-controller.url/invitation-url 호출부터 시작 | | | connections, response | connections, response | | | | connections, active | connections, active | -### Presentation +### Presentation before Issue Credential Holder가 (connections, active) 시점에 credential proposal을 보냄 | Issuer API | Holder API | Issuer webhook (topic, state, *msg_type) | Holder webhook (topic, state, *msg_type) | |---|---|---|---| | | POST /issue-credential/send-proposal | issue_credential, proposal_received | issue_credential, proposal_sent | -| POST /connections/{conn_id}/send-message | | | basicmessages, received, *initial_agreement | -| | POST /connections/{conn_id}/send-message | basicmessages, received, *initial_agreement_decision | | | POST /present-proof/send-verification-request | | present_proof, request_sent | present_proof, request_received | | | GET /present-proof/records/{presExId}/credentials | | | | | POST /present-proof/records/{presExId}/send-presentation | present_proof, presentation_received | present_proof, presentation_sent | @@ -148,7 +186,7 @@ Issuer는 받은 정보를 기반으로 DB를 query 하여 증명서를 작성 | | | issuer_cred_rev, issued | | | | | issue_credential, credential_acked | issue_credential, credential_acked | -발급한 증명서를 폐기(revocation)하기 위해, \ +추후 발급한 증명서를 폐기(revocation)하기 위해서는, \ Issuer는 (issue_credential, credential_acked) 시점에 webhook 메시지를 확인하여 credential_exchange_id 를 DB에 기록해 두어야 함 ### (Optional) Revocation @@ -158,6 +196,34 @@ Issuer는 (issue_credential, credential_acked) 시점에 webhook 메시지를 Revoke된 credential은 Issuer가 (present_proof, verified) 시점에, webhook 메시지를 getPresentationResult 하는 과정에서 verified 가 false 임 +## Verifier Work flow +### Initialization +Verifier는 accessToken, credDefId, verifTplId, webhookUrl 이 valid 한 지 확인 하고 대기함. + +### Connection +Holder가 https://verifier-controller.url/invitation-url 호출부터 시작 + +| Issuer API | Holder API | Issuer webhook (topic, state) | Holder webhook (topic, state) | +|---|---|---|---| +| POST /connections/create-invitation | | | | +| | POST /connections/receive-invitation | | connections, invitation | +| | | connections, request | connections, request | +| | | connections, response | connections, response | +| | | connections, active | connections, active | + +### Presentation +Holder가 (connections, active) 시점에 presentation proposal을 보냄 + +| Issuer API | Holder API | Issuer webhook (topic, state, *msg_type) | Holder webhook (topic, state, *msg_type) | +|---|---|---|---| +| | POST /present-proof/send-proposal | present_proof, proposal_received | present_proof, proposal_sent | +| POST /present-proof/send-verification-request | | present_proof, request_sent | present_proof, request_received | +| | GET /present-proof/records/{presExId}/credentials | | | +| | POST /present-proof/records/{presExId}/send-presentation | present_proof, presentation_received | present_proof, presentation_sent | +| | | present_proof, verified | present_proof, presentation_acked | + +Verifier는 (present_proof, verified) 시점에 webhook 메시지를 getPresentationResult 하여 요구한 정보 획득 + ## Production 뱐경 해야 할 항목만 정리 @@ -174,16 +240,16 @@ production 새로 작성 #### application-issuer-prod.properties agentApiUrl = https://console.myinitial.io/agent/api (고정) -#### application-holder-prod.properties +#### application-verifier-prod.properties agentApiUrl = https://console.myinitial.io/agent/api (고정) -### 3. issuer 및 holder 실행 +### 3. issuer 또는 verifier 실행 #### issuer 실행 ``` ./gradlew issuer_prod ``` -#### holder 실행 +#### verifier 실행 ``` -./gradlew holder_prod +./gradlew verifier_prod ``` diff --git a/build.gradle b/build.gradle index 51bd015..83b0435 100644 --- a/build.gradle +++ b/build.gradle @@ -23,16 +23,15 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' + compileOnly 'org.projectlombok:lombok:1.18.24' + annotationProcessor 'org.projectlombok:lombok:1.18.24' - compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.8.0' - compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6' - implementation 'com.jayway.jsonpath:json-path:2.4.0' - implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.10' - compile 'com.google.zxing:javase:3.3.3' - compile group: 'commons-codec', name: 'commons-codec', version: '1.15' - implementation group: 'commons-io', name: 'commons-io', version: '2.6' + implementation 'com.squareup.okhttp3:okhttp:4.10.0' + implementation 'com.google.code.gson:gson:2.9.0' + implementation 'com.jayway.jsonpath:json-path:2.7.0' + implementation 'org.apache.commons:commons-lang3:3.12.0' + implementation 'commons-codec:commons-codec:1.15' + implementation 'commons-io:commons-io:2.11.0' } test { @@ -65,15 +64,26 @@ task issuer_prod(type:JavaExec) { main = 'com.sktelecom.initial.controller.issuer.Application' args(['--spring.config.location=classpath:/application-issuer-prod.properties']) } +task verifier(type:JavaExec) { + group 'application' + classpath sourceSets.main.runtimeClasspath + main = 'com.sktelecom.initial.controller.verifier.Application' + args(['--spring.config.location=classpath:/application-verifier.properties']) +} +task verifier_prod(type:JavaExec) { + group 'application' + classpath sourceSets.main.runtimeClasspath + main = 'com.sktelecom.initial.controller.verifier.Application' + args(['--spring.config.location=classpath:/application-verifier-prod.properties']) +} task holder(type:JavaExec) { group 'application' classpath sourceSets.main.runtimeClasspath main = 'com.sktelecom.initial.controller.holder.Application' - args(['--spring.config.location=classpath:/application-holder.properties']) } -task holder_prod(type:JavaExec) { +task holder_webhook(type:JavaExec) { group 'application' classpath sourceSets.main.runtimeClasspath - main = 'com.sktelecom.initial.controller.holder.Application' - args(['--spring.config.location=classpath:/application-holder-prod.properties']) + main = 'com.sktelecom.initial.controller.holder_webhook.Application' + args(['--spring.config.location=classpath:/application-holder_webhook.properties']) } \ No newline at end of file diff --git a/src/main/java/com/sktelecom/initial/controller/holder/Application.java b/src/main/java/com/sktelecom/initial/controller/holder/Application.java index 7be2ceb..30ba141 100644 --- a/src/main/java/com/sktelecom/initial/controller/holder/Application.java +++ b/src/main/java/com/sktelecom/initial/controller/holder/Application.java @@ -1,15 +1,584 @@ package com.sktelecom.initial.controller.holder; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.PathNotFoundException; +import com.sktelecom.initial.controller.utils.Common; +import com.sktelecom.initial.controller.utils.HttpClient; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.LinkedHashMap; -@SpringBootApplication -@Slf4j public class Application { + static Logger log = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + static final HttpClient client = new HttpClient(); + + // CONFIGURATION + // EDIT THIS + static final String appMode = "dev"; // dev prod + static final String runType = "issue"; // issue verify + static final String logLevel = "INFO"; // INFO DEBUG + + // Case of runType issue (모바일가입증명 제출 후 발급) + static String tpIssuerInvitationUrl = "http://221.168.33.105:8043/invitation-url"; // test issuer (default: dev degree issuer) + static String tpCredDefId = "SxK6LzvMTgmCPTBm3LwRvG:3:CL:1618984624:43769670-fa4d-4aa8-94c6-e281a46726f0"; // test issuer (default: dev degree issuer) + + // Case of runType verify (모바일가입증명 제출 후 완료) + static String tpVerifierInvitationUrl = "http://221.168.33.105:8041/invitation-url"; // test verifier (default: dev mobile verifier) + + // STATIC + // DO NOT CHANGE THIS + static String mwpUrl; + static String authUrl; + static String agentApiUrl; + static String agentDataStoreUrl; + + static String authUsername; + static String authPassword; + static String testUserCI; + + static String mobileIssuerInvitationUrlApi; + static String mobileCredDefId; + + static String mobileCredProposalSite; + static String mobileCredProposalReqSeq; + static String mobileCredProposalEncodeData; + + + static int pollingCyclePeriod = 1000; // 1 second + static int pollingRetryMax = 100; // 100 times + + static String accessToken; + public static void main(String[] args) { - SpringApplication.run(Application.class, args); - // GlobalService.initializeAfterStartup() is automatically called after this application starts. + // Set log level + switch (logLevel) { + case "DEBUG": + log.setLevel(Level.DEBUG); + break; + default: + log.setLevel(Level.INFO); + } + + // Set configuration + switch (appMode) { + case "dev": + mwpUrl = "https://dev.mobile-wallet.co.kr"; // dev + authUsername = "4e37046d-4dca-4fd0-b69f-df2ee1b2"; // dev + authPassword = "6ea32d8d-4a04-4a0c-a664-50a950ee"; // dev + mobileIssuerInvitationUrlApi = "https://dev-console.myinitial.io/mobile-issuer-v2/invitation-url"; // dev + mobileCredDefId = "TBz5HEP6gzwqDDMw3Ci7BU:3:CL:1618987943:4224f310-cd2b-4836-843b-07b666c2bf6b"; // dev + mobileCredProposalEncodeData = "9INN4y9uwW5M6lZ6GvvaLUjfPoq25xDuD2XZVF7GlpIbKLwf7SJJgRoIhnx3hXwYfc99b5HQ0QFSMQb4BoDXjnV3/WgeAXjgeP4WACN0OSyneD8DhB9nRmda1zkQVQ2WseKtjLD4FZRsbtZWVu5m5fJ1+P76bmYd7nFA3p1HX6oUocY0QVbGhsYRr/nCNccr/5+zOc2ghzVpD2P3KREOx6rZD1DDWWR2PMl6GOth3QXqF9aXSBdbXWKi9imG/QfsVDQwNxbqY6i2yRfkjGYM1P+eIKknUoNJueMzDgEib3K7sV7YZUm+KH2K7ePvAs09"; // dev + break; + case "prod": + mwpUrl = "https://www.mobile-wallet.co.kr"; // prod + authUsername = "092d9926-f73c-46ea-9c1a-11479de8"; // prod + authPassword = "d74c243a-d282-43d3-bdde-eb749c78"; // prod + mobileIssuerInvitationUrlApi = "https://console.myinitial.io/mobile-issuer-v2/invitation-url"; // prod + mobileCredDefId = "SWiirNiJX7PdVS6Ji8a5tB:3:CL:101:6e43d3b8-ca11-48b5-83be-5adb240520ad"; // prod + mobileCredProposalEncodeData = "X4BWcp2XFp77xtGlaSTHNmWuziTyQTkW8x9XRHd7OJnaFIoDCv6yUPbBBNV7JEVtNUMdY4goKKEO65A5ctIrtJOxP9DdUv02q7MWaV+J4ML9d+hfhrd7K0BDuD/P1hUssCBTS4dVNJII5VRvCmrnoRKtndfitUqRbYPnCAvUBvRJSxW21fjM2Q40D3wLOb4OdvXS/NY+nB/Xj2QPuYl594zdAEODgDrjdjw+LIILl486HHG+LUgg/jHSn/n7XYk7aWCu2ZHalJIrglalPldTrey/fZSH1rpBf4jUf5LskvULFneq/gvNSL2MQTkYoJR4"; // prod + break; + default: + log.error("invalid appMode"); + System.exit(-1); + } + // 공통 + authUrl = mwpUrl + "/auth"; + agentApiUrl = mwpUrl + "/agent/api"; + agentDataStoreUrl = mwpUrl + "/agent/ds"; + testUserCI = "43aZWK3vEJKQCpaCc91kagoiz4SL6BQ8ibDmbksjRpfGo5UDAga5WJhD8v71rjxamc2Twr8YiciuXqVg4PaYXDfB"; + mobileCredProposalSite = "mobile-wallet"; + mobileCredProposalReqSeq = "REQ_SEQ_VALUE"; + + // Login to get access token + log.info("Login with testUserCI to get access token"); + accessToken = getAccessToken(); + log.info("access token: " + accessToken); + + // Receive mobile identification credential + log.info("Receive mobile identification credential"); + String mobileCredId = receiveMobileCredential(); + printCredentialByCredId(mobileCredId); + + // testing issuer or verifier + if (runType.equals("issue")) { + runIssueProcess(); + } + else if (runType.equals("verify")) { + runVerifyProcess(); + } + + log.info("Delete mobile identification credential for clean up"); + deleteCredential(mobileCredId); + + log.info("Print credential history"); + printCredentialHistory(); + + log.info("Demo completed successfully"); + } + + static String getAccessToken() { + String form = "username=" + testUserCI + + "&grant_type=password" + + "&scope=all"; + String response = client.requestPOSTBasicAuth(authUrl + "/oauth2/token", authUsername, authPassword, form); + log.debug("response: " + response); + return JsonPath.read(response, "$.access_token"); + } + + static String receiveMobileCredential() { + log.info("--- Preparation Process Start ---"); + + // Establish Connection + log.info("[mobile credential] Receive invitation to establish connection"); + String connectionId = receiveInvitation(mobileIssuerInvitationUrlApi); + log.info("[mobile credential] connection id: " + connectionId); + waitUntilConnectionState(connectionId, "active"); + log.info("[mobile credential] connection established"); + + // Receive Credential + log.info("[mobile credential] Send credential proposal to receive credential offer"); + String credExId = sendMobileCredentialProposal(connectionId); + log.info("[mobile credential] credential exchange id: " + credExId); + waitUntilCredentialExchangeState(credExId, "offer_received"); + + log.info("[mobile credential] Send credential request to receive credential"); + sendCredentialRequest(credExId); + waitUntilCredentialExchangeState(credExId, "credential_acked"); + log.info("[mobile credential] credential received"); + + log.info("[mobile credential] Delete connection for clean up"); + deleteConnection(connectionId); + + log.info("--- Preparation Process End ---"); + + return getCredentialIdByCredExId(credExId); + } + + static void runIssueProcess() { + log.info("--- Issue Process Start ---"); + + // Establish Connection + log.info("Receive invitation to establish connection"); + String connectionId = receiveInvitation(tpIssuerInvitationUrl); + log.info("connection id: " + connectionId); + waitUntilConnectionState(connectionId, "active"); + log.info("connection established"); + + // Send Credential Proposal + log.info("Send credential proposal"); + String credExId = sendTpCredentialProposal(connectionId); + log.info("credential exchange id: " + credExId); + + // Send Presentation of mobile identification credential + log.info("Receive presentation request for mobile identification credential"); + String presExId = receiveIdByTopicAndState(connectionId, "present_proof", "request_received"); + log.info("presentation exchange id: " + presExId); + + log.info("Print agreement"); + printAgreement(presExId); + + log.info("Print self attested attributes"); + printSelfAttestedAttrs(presExId); + + log.info("Send presentation"); + sendPresentation(presExId); + waitUntilPresentationExchangeState(presExId, "presentation_acked"); + log.info("presentation acked"); + + // Receive last Event + log.info("Receive last event to check the topic is basicmessages or issue_credential"); + String topic = receiveTopicByCondition(connectionId); + + if (topic.equals("basicmessages")) { + // (Webview) Receive Basic Message + log.info("Receive basic message to get webview url"); + String msgId = receiveIdByTopicAndState(connectionId, "basicmessages", "received"); + printWebviewUrl(msgId); + log.info("Click above webviewUrl and Submit"); + } + + // Receive Credential + waitUntilCredentialExchangeState(credExId, "offer_received"); + log.info("Send credential request to receive credential"); + sendCredentialRequest(credExId); + waitUntilCredentialExchangeState(credExId, "credential_acked"); + log.info("credential received"); + String tpCredId = getCredentialIdByCredExId(credExId); + printCredentialByCredId(tpCredId); + + log.info("Delete third party credential for clean up"); + deleteCredential(tpCredId); + + log.info("Delete connection for clean up"); + deleteConnection(connectionId); + + log.info("--- Issue Process End ---"); + } + + static void runVerifyProcess() { + log.info("--- Verify Process Start ---"); + + // Establish Connection + log.info("Receive invitation to establish connection"); + String connectionId = receiveInvitation(tpVerifierInvitationUrl); + log.info("connection id: " + connectionId); + waitUntilConnectionState(connectionId, "active"); + log.info("connection established"); + + // Send Presentation Proposal + log.info("Send presentation proposal"); + sendMobilePresentationProposal(connectionId); + + // Send Presentation of mobile identification credential + log.info("Receive presentation request for mobile identification credential"); + String presExId = receiveIdByTopicAndState(connectionId, "present_proof", "request_received"); + log.info("presentation exchange id: " + presExId); + + log.info("Print agreement"); + printAgreement(presExId); + + log.info("Print self attested attributes"); + printSelfAttestedAttrs(presExId); + + log.info("Send presentation"); + sendPresentation(presExId); + waitUntilPresentationExchangeState(presExId, "presentation_acked"); + log.info("presentation acked"); + + log.info("Delete connection for clean up"); + deleteConnection(connectionId); + + log.info("--- Verify Process End ---"); + } + + static void printCredentialByCredId(String credId) { + String response = client.requestGET(agentApiUrl + "/credential/" + credId, accessToken); + log.info("credential: " + response); + } + + static String receiveInvitation(String invitationUrlApi) { + String invitationUrl = client.requestGET(invitationUrlApi, ""); + log.debug("invitation-url: " + invitationUrl); + String invitation = Common.parseInvitationUrl(invitationUrl); + log.info("invitation: " + invitation); + + String response = client.requestPOST(agentApiUrl + "/connections/receive-invitation", accessToken, invitation); + log.debug("response: " + response); + return JsonPath.read(response, "$.connection_id"); + } + + static String sendMobileCredentialProposal(String connectionId) { + String comment = JsonPath.parse("{" + + " site: '" + mobileCredProposalSite + "'," + + " req_seq: '" + mobileCredProposalReqSeq + "'," + + " encode_data: '" + mobileCredProposalEncodeData + "'," + + "}").jsonString(); + String body = JsonPath.parse("{" + + " connection_id: '" + connectionId + "'," + + " cred_def_id: '" + mobileCredDefId + "'," + + " comment: '" + comment + "'," + + "}").jsonString(); + String response = client.requestPOST(agentApiUrl + "/issue-credential/send-proposal", accessToken, body); + log.debug("response: " + response); + return JsonPath.read(response, "$.credential_exchange_id"); + } + + static String sendTpCredentialProposal(String connectionId) { + String body = JsonPath.parse("{" + + " connection_id: '" + connectionId + "'," + + " cred_def_id: '" + tpCredDefId + "'," + + "}").jsonString(); + String response = client.requestPOST(agentApiUrl + "/issue-credential/send-proposal", accessToken, body); + log.debug("response: " + response); + return JsonPath.read(response, "$.credential_exchange_id"); + } + + static void sendMobilePresentationProposal(String connectionId) { + String body = JsonPath.parse("{" + + " connection_id: '" + connectionId + "'," + + " presentation_proposal: {" + + " attributes: []," + + " predicates: []" + + " }" + + "}").jsonString(); + String response = client.requestPOST(agentApiUrl + "/present-proof/send-proposal", accessToken, body); + log.debug("response: " + response); + } + + static void sendCredentialRequest(String credExId) { + String response = client.requestPOST(agentApiUrl + "/issue-credential/records/" + credExId + "/send-request", accessToken, "{}"); + log.debug("response: " + response); + } + + static String getCredentialIdByCredExId(String credExId) { + String response = client.requestGET(agentDataStoreUrl + "/issue-credential/records/" + credExId, accessToken); + log.debug("response: " + response); + return JsonPath.read(response, "$.credential.referent"); + } + + static void sendPresentation(String presExId) { + String response = client.requestGET(agentDataStoreUrl + "/present-proof/records/" + presExId, accessToken); + log.debug("response: " + response); + String presentationRequest = JsonPath.parse((LinkedHashMap)JsonPath.read(response, "$.presentation_request")).jsonString(); + + response = client.requestGET(agentApiUrl + "/present-proof/records/" + presExId + "/credentials", accessToken); + log.info("Matching Credentials in my wallet: " + response); + + ArrayList> credentials = JsonPath.read(response, "$"); + int credRevId = 0; + String credId = null; + for (LinkedHashMap element : credentials) { + if (JsonPath.read(element, "$.cred_info.cred_rev_id") != null){ // case of support revocation + int curCredRevId = Integer.parseInt(JsonPath.read(element, "$.cred_info.cred_rev_id")); + if (curCredRevId > credRevId) { + credRevId = curCredRevId; + credId = JsonPath.read(element, "$.cred_info.referent"); + } + } + else { // case of not support revocation + credId = JsonPath.read(element, "$.cred_info.referent"); + } + } + log.info("Use latest credential in demo - credential_id: "+ credId); + + // Make body using presentationRequest + LinkedHashMap reqAttrs = JsonPath.read(presentationRequest, "$.requested_attributes"); + for(String key : reqAttrs.keySet()) + reqAttrs.replace(key, JsonPath.parse("{ cred_id: '" + credId + "', revealed: true }").json()); + + LinkedHashMap reqPreds = JsonPath.read(presentationRequest, "$.requested_predicates"); + for(String key : reqPreds.keySet()) + reqPreds.replace(key, JsonPath.parse("{ cred_id: '" + credId + "' }").json()); + + LinkedHashMap selfAttrs = new LinkedHashMap<>(); + + String body = JsonPath.parse("{}").put("$", "requested_attributes", reqAttrs) + .put("$", "requested_predicates", reqPreds) + .put("$", "self_attested_attributes", selfAttrs).jsonString(); + + response = client.requestPOST(agentApiUrl + "/present-proof/records/" + presExId + "/send-presentation", accessToken, body); + log.debug("response: " + response); + } + + static void printWebviewUrl(String msgId) { + String response = client.requestGET(agentDataStoreUrl + "/basic-messages/" + msgId, accessToken); + log.debug("response: " + response); + String content = JsonPath.read(response, "$.content"); + String contentType = JsonPath.read(content, "$.type"); + if (!contentType.equals("initial_web_view")) { + log.error("contentType is not initial_web_view"); + System.exit(-1); + } + String contentContent = JsonPath.parse((LinkedHashMap)JsonPath.read(content, "$.content")).jsonString(); + String webviewUrl = JsonPath.read(contentContent, "$.web_view_url"); + log.info("WebviewUrl: " + webviewUrl); + + // Update basic message state to done + String body = JsonPath.parse("{ state: 'done' }").jsonString(); + response = client.requestPOST(agentDataStoreUrl + "/basic-messages/" + msgId + "/update", accessToken, body); + log.debug("response: " + response); + } + + static void printAgreement(String presExId) { + String response = client.requestGET(agentDataStoreUrl + "/present-proof/records/" + presExId, accessToken); + log.debug("response: " + response); + String comment = JsonPath.read(response, "$.presentation_request_dict.comment"); + try { + String agreement = JsonPath.parse((LinkedHashMap) JsonPath.read(comment, "$.agreement")).jsonString(); + log.info("agreement: " + agreement); + } catch (PathNotFoundException ignored){} + } + + static void printSelfAttestedAttrs(String presExId) { + String response = client.requestGET(agentDataStoreUrl + "/present-proof/records/" + presExId, accessToken); + log.debug("response: " + response); + String comment = JsonPath.read(response, "$.presentation_request_dict.comment"); + try { + String selfAttestedAttrs = JsonPath.parse((ArrayList) JsonPath.read(comment, "$.self_attested_attrs")).jsonString(); + log.info("self attested attributes: " + selfAttestedAttrs); + } catch (PathNotFoundException ignored){} + } + + static void deleteCredential(String credId) { + String response = client.requestDELETE(agentApiUrl + "/credential/" + credId, accessToken); + log.debug("response: " + response); + log.info("credId:" + credId + " is deleted"); + } + + static void deleteConnection(String connId) { + String response = client.requestDELETE(agentApiUrl + "/connections/" + connId, accessToken); + log.debug("response: " + response); + log.info("connId:" + connId + " is deleted"); + } + + static void waitUntilConnectionState(String connectionId, String state) { + log.info("Wait until connection (state: " + state + ")"); + for (int retry=0; retry < pollingRetryMax; retry++) { + String response = client.requestGET(agentDataStoreUrl + "/connections/" + connectionId, accessToken); + log.debug("response: " + response); + String resState = JsonPath.read(response, "$.state"); + log.info("connection state:" + resState); + if (resState.equals(state)) + return; + Common.sleep(pollingCyclePeriod); + } + log.error("timeout - connection is not (state: " + state + ")"); + System.exit(-1); + } + + static void waitUntilCredentialExchangeState(String credExId, String state) { + log.info("Wait until credential exchange (state: " + state + ")"); + for (int retry=0; retry < pollingRetryMax; retry++) { + String response = client.requestGET(agentDataStoreUrl + "/issue-credential/records/" + credExId, accessToken); + log.debug("response: " + response); + String resState = JsonPath.read(response, "$.state"); + log.info("credential exchange state: " + resState); + if (resState.equals("abandoned")) { + log.info("error message: " + JsonPath.read(response, "$.error_msg")); + System.exit(-1); + } + if (resState.equals(state)) + return; + Common.sleep(pollingCyclePeriod); + } + log.error("timeout - credential exchange is not (state: " + state + ")"); + System.exit(-1); + } + + static void waitUntilPresentationExchangeState(String presExId, String state) { + log.info("Wait until presentation exchange (state: " + state + ")"); + for (int retry=0; retry < pollingRetryMax; retry++) { + String response = client.requestGET(agentDataStoreUrl + "/present-proof/records/" + presExId, accessToken); + log.debug("response: " + response); + String resState = JsonPath.read(response, "$.state"); + log.info("presentation exchange state: " + resState); + if (resState.equals("abandoned")) { + log.info("error message: " + JsonPath.read(response, "$.error_msg")); + System.exit(-1); + } + if (resState.equals(state)) + return; + Common.sleep(pollingCyclePeriod); + } + log.error("timeout - presentation exchange is not (state: " + state + ")"); + System.exit(-1); + } + + static String receiveIdByTopicAndState(String connectionId, String topic, String state) { + log.info("Wait until last event (" + topic + ", " + state + ")"); + for (int retry=0; retry < pollingRetryMax; retry++) { + String params = "?connection_id=" + connectionId; + params += "&topic=" + topic; + String response = client.requestGET(agentDataStoreUrl + "/events/last" + params, accessToken); + log.debug("response: " + response); + // no event yet + if (response == null) { + Common.sleep(pollingCyclePeriod); + continue; + } + String resTopic = JsonPath.read(response, "$.topic"); + String resState = JsonPath.read(response, "$.state"); + log.info("last event: (" + resTopic + ", " + resState + ")"); + if (resState.equals("abandoned")) { + log.info("error message: " + JsonPath.read(response, "$.error_msg")); + System.exit(-1); + } + if (resTopic.equals(topic) && resState.equals(state)) { + switch (topic) { + case "issue_credential": + return JsonPath.read(response, "$.credential_exchange_id"); + case "present_proof": + return JsonPath.read(response, "$.presentation_exchange_id"); + case "basicmessages": + return JsonPath.read(response, "$.message_id"); + default: + return null; + } + } + Common.sleep(pollingCyclePeriod); + } + log.error("timeout - last event is not (" + topic + ", " + state + ")"); + System.exit(-1); + return null; + } + + static String receiveTopicByCondition(String connectionId) { + log.info("Wait until last event (basicmessages, received) or (issue_credential, offer_received)"); + for (int retry=0; retry < pollingRetryMax; retry++) { + String params = "?connection_id=" + connectionId; + params += "&topic=issue_credential,basicmessages"; + String response = client.requestGET(agentDataStoreUrl + "/events/last" + params, accessToken); + log.debug("response: " + response); + // no event yet + if (response == null) { + Common.sleep(pollingCyclePeriod); + continue; + } + String resTopic = JsonPath.read(response, "$.topic"); + String resState = JsonPath.read(response, "$.state"); + log.info("last event: (" + resTopic + ", " + resState + ")"); + if (resState.equals("abandoned")) { + log.info("error message: " + JsonPath.read(response, "$.error_msg")); + System.exit(-1); + } + if ((resTopic.equals("basicmessages") && resState.equals("received")) + || (resTopic.equals("issue_credential") && resState.equals("offer_received"))) { + return resTopic; + } + Common.sleep(pollingCyclePeriod); + } + log.error("timeout - last event is not (basicmessages, received) or (issue_credential, offer_received)"); + System.exit(-1); + return null; + } + + static void printCredentialHistory() { + String params = "?page=1&page_size=10"; + String response = client.requestGET(agentDataStoreUrl + "/credential-histories" + params, accessToken); + log.debug("response: " + response); + log.info("credential history: " + JsonPath.read(response, "$.results")); + } + + // Unused functions + static void deleteCredentialsByCredDefId(String credDefId) { + String wql = JsonPath.parse("{" + + " cred_def_id: '" + credDefId + "'," + + "}").jsonString(); + String params = "?wql=" + wql; + String response = client.requestGET(agentApiUrl + "/credentials" + params, accessToken); + log.debug("response: " + response); + ArrayList> creds = JsonPath.read(response, "$.results"); + for (LinkedHashMap cred : creds) { + String credId = JsonPath.read(cred, "$.referent"); + response = client.requestDELETE(agentApiUrl + "/credential/" + credId, accessToken); + log.debug("response: " + response); + log.info("deleted credId: " + credId); + } + } + + // Unused functions + static void deleteAllConnections() { + String response = client.requestGET(agentApiUrl + "/connections", accessToken); + log.debug("response: " + response); + ArrayList> conns = JsonPath.read(response, "$.results"); + for (LinkedHashMap conn : conns) { + String connId = JsonPath.read(conn, "$.connection_id"); + deleteConnection(connId); + } + } + + // Unused functions + static void deleteAllCredentials() { + String response = client.requestGET(agentApiUrl + "/credentials", accessToken); + log.debug("response: " + response); + ArrayList> creds = JsonPath.read(response, "$.results"); + for (LinkedHashMap cred : creds) { + String credId = JsonPath.read(cred, "$.referent"); + deleteCredential(credId); + } } } diff --git a/src/main/java/com/sktelecom/initial/controller/holder/GlobalService.java b/src/main/java/com/sktelecom/initial/controller/holder/GlobalService.java deleted file mode 100644 index c0b9938..0000000 --- a/src/main/java/com/sktelecom/initial/controller/holder/GlobalService.java +++ /dev/null @@ -1,343 +0,0 @@ -package com.sktelecom.initial.controller.holder; - -import com.jayway.jsonpath.JsonPath; -import com.jayway.jsonpath.PathNotFoundException; -import com.sktelecom.initial.controller.utils.HttpClient; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.Timer; -import java.util.TimerTask; - -import static com.sktelecom.initial.controller.utils.Common.*; - -@RequiredArgsConstructor -@Service -@Slf4j -public class GlobalService { - private final HttpClient client = new HttpClient(); - - // sample mobile credential definition identifier - final String sampleMobileCredDefId = "4UvAbFrGzyzH6zhvfDSvxf:3:CL:1618987943:707ca438-2198-465c-8e8d-1b8ab8cef021"; - // sample mobile credential issuer controller url - final String sampleMobileIssuerInvitationUrl = "http://221.168.33.78:8044/invitation-url"; - - @Value("${agentApiUrl}") - private String agentApiUrl; // agent service api url - - @Value("${accessToken}") - private String accessToken; // controller access token - - @Value("${issuerInvitationUrl}") - private String issuerInvitationUrl; // issuer controller invitation url to receive invitation-url - - @Value("${issuerCredDefId}") - private String issuerCredDefId; // credential definition identifier to receive - - String orgName; - String orgImageUrl; - String publicDid; - String phase; - - @EventListener(ApplicationReadyEvent.class) - public void initialize() { - provisionController(); - - log.info("Controller is ready"); - log.info("Controller configurations"); - log.info("------------------------------"); - log.info("- organization name: " + orgName); - log.info("- organization imageUrl: " + orgImageUrl); - log.info("- public did: " + publicDid); - log.info("- controller access token: " + accessToken); - log.info("- issuer controller invitation url: " + issuerInvitationUrl); - log.info("- credential definition id to receive from issuer: " + issuerCredDefId); - log.info("------------------------------"); - - log.info("Preparation - start"); - phase = "preparation"; - if (existSampleMobileCredential()) { - log.info("Use existing sample mobile credential"); - log.info("Preparation - done"); - startDemo(); - } - else { - log.info("Receive sample mobile credential"); - receiveInvitationUrl(sampleMobileIssuerInvitationUrl); - } - } - - public void handleEvent(String body) { - String topic = JsonPath.read(body, "$.topic"); - String state = topic.equals("problem_report") ? null : JsonPath.read(body, "$.state"); - log.info("handleEvent >>> topic:" + topic + ", state:" + state + ", body:" + body); - - switch(topic) { - case "connections": - // 1. connection 이 완료됨 -> credential 을 요청함 - if (state.equals("active")) { - log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendCredentialProposal"); - sendCredentialProposal(JsonPath.read(body, "$.connection_id"), issuerCredDefId); - } - break; - case "issue_credential": - // 4-2. 증명서 preview 받음 -> 증명서 요청 - if (state.equals("offer_received")) { - log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendCredentialRequest"); - sendCredentialRequest(JsonPath.read(body, "$.credential_exchange_id")); - } - // 4-3. 증명서를 정상 저장하였음 -> 완료 - else if (state.equals("credential_acked")) { - log.info("- Case (topic:" + topic + ", state:" + state + ") -> credential received successfully"); - delayedExit(); - } - break; - case "basicmessages": - String content = JsonPath.read(body, "$.content"); - String type = getTypeFromBasicMessage(content); - // 2. 개인정보이용 동의 요청 받음 -> 동의하여 전송 - if (type != null && type.equals("initial_agreement")) { - log.info("- Case (topic:" + topic + ", state:" + state + ", type:" + type + ") -> sendAgreementAgreed"); - sendAgreementAgreed(JsonPath.read(body, "$.connection_id"), content); - } - // 4-1. web view를 통한 추가 정보 요구 -> 선택하여 전송 - else if (type != null && type.equals("initial_web_view")) { - log.info("- Case (topic:" + topic + ", state:" + state + ", type:" + type + ") -> showWebViewAndSelect"); - showWebViewAndSelect(content); - } - else - log.warn("- Warning: Unexpected type:" + type); - break; - case "present_proof": - // 3. 모바일 가입증명 검증 요청 받음 -> 모바일 가입 증명 검증 전송 - if (state.equals("request_received")) { - log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendPresentation"); - String presentationRequest = JsonPath.parse((LinkedHashMap)JsonPath.read(body, "$.presentation_request")).jsonString(); - sendPresentation(JsonPath.read(body, "$.presentation_exchange_id"), presentationRequest); - } - break; - case "problem_report": - log.warn("- Case (topic:" + topic + ") -> Print body"); - log.warn(" - body:" + prettyJson(body)); - break; - case "revocation_registry": - case "issuer_cred_rev": - break; - default: - log.warn("- Warning: Unexpected topic:" + topic); - } - } - - public void handleEventOnPreparation(String body) { - String topic = JsonPath.read(body, "$.topic"); - String state = topic.equals("problem_report") ? null : JsonPath.read(body, "$.state"); - log.info("handleEvent >>> topic:" + topic + ", state:" + state + ", body:" + body); - - switch(topic) { - case "connections": - if (state.equals("active")) { - log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendCredentialProposal"); - sendCredentialProposal(JsonPath.read(body, "$.connection_id"), sampleMobileCredDefId); - } - break; - case "issue_credential": - if (state.equals("offer_received")) { - log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendCredentialRequest"); - sendCredentialRequest(JsonPath.read(body, "$.credential_exchange_id")); - } - else if (state.equals("credential_acked")) { - log.info("- Case (topic:" + topic + ", state:" + state + ") -> sample mobile credential received successfully"); - log.info("Preparation - done"); - startDemo(); - } - break; - case "problem_report": - log.warn("- Case (topic:" + topic + ") -> Print body"); - log.warn(" - body:" + prettyJson(body)); - break; - case "basicmessages": - case "present_proof": - case "revocation_registry": - case "issuer_cred_rev": - break; - default: - log.warn("- Warning Unexpected topic:" + topic); - } - } - - public void provisionController() { - log.info("TEST - Create invitation-url"); - String invitationUrl = createInvitationUrl(); - if (invitationUrl == null) { - log.info("- FAILED: Check if accessToken is valid - " + accessToken); - System.exit(0); - } - String invitation = parseInvitationUrl(invitationUrl); - publicDid = JsonPath.read(invitation, "$.did"); - orgName = JsonPath.read(invitation, "$.label"); - orgImageUrl = JsonPath.read(invitation, "$.imageUrl"); - log.info("- SUCCESS"); - } - - public String createInvitationUrl() { - String params = "?public=true"; - String response = client.requestPOST(agentApiUrl + "/connections/create-invitation" + params, accessToken, "{}"); - log.info("response: " + response); - try { - return JsonPath.read(response, "$.invitation_url"); - } catch (IllegalArgumentException e) { - return null; - } - } - - public boolean existSampleMobileCredential() { - String response = client.requestGET(agentApiUrl + "/credentials", accessToken); - log.info("response: " + response); - - ArrayList credentials = JsonPath.read(response, "$.results"); - for (Object element : credentials) { - String credDefId = JsonPath.read(element, "$.cred_def_id"); - if (credDefId.equals(sampleMobileCredDefId)) - return true; - } - return false; - } - - public void startDemo() { - phase = "started"; - log.info("Receive invitation from issuer controller"); - receiveInvitationUrl(issuerInvitationUrl); - } - - public void receiveInvitationUrl(String controllerInvitationUrl) { - String invitationUrl = client.requestGET(controllerInvitationUrl, ""); - if (invitationUrl == null) { - log.warn("Invalid invitation-url"); - return; - } - log.info("invitation-url: " + invitationUrl); - String invitation = parseInvitationUrl(invitationUrl); - log.info("invitation: " + invitation); - String response = client.requestPOST(agentApiUrl + "/connections/receive-invitation", accessToken, invitation); - log.info("response: " + response); - } - - public void sendCredentialProposal(String connectionId, String credDefId) { - String body = JsonPath.parse("{" + - " connection_id: '" + connectionId + "'," + - " cred_def_id: '" + credDefId + "'," + - "}").jsonString(); - String response = client.requestPOST(agentApiUrl + "/issue-credential/send-proposal", accessToken, body); - log.info("response: " + response); - } - - public void sendAgreementAgreed(String connectionId, String content) { - try { - String agreementContent = JsonPath.parse((LinkedHashMap)JsonPath.read(content, "$.content")).jsonString(); - log.info("agreementContent: " + agreementContent); - } catch (PathNotFoundException e) { - log.warn("Invalid content format -> Ignore"); - return; - } - - // assume user accept this agreement - String initialAgreementDecision = JsonPath.parse("{" + - " type: 'initial_agreement_decision'," + - " content: {" + - " agree_yn :'Y'," + - " signature:'message signature',"+ - " }" + - "}").jsonString(); - String body = JsonPath.parse("{ content: '" + initialAgreementDecision + "' }").jsonString(); - String response = client.requestPOST(agentApiUrl + "/connections/" + connectionId + "/send-message", accessToken, body); - log.info("response: " + response); - } - - public void sendPresentation(String presExId, String presentationRequest) { - String response = client.requestGET(agentApiUrl + "/present-proof/records/" + presExId + "/credentials", accessToken); - log.info("Matching Credentials in my wallet: " + response); - - ArrayList> credentials = JsonPath.read(response, "$"); - int credRevId = 0; - String credId = null; - for (LinkedHashMap element : credentials) { - if (JsonPath.read(element, "$.cred_info.cred_rev_id") != null){ // case of support revocation - int curCredRevId = Integer.parseInt(JsonPath.read(element, "$.cred_info.cred_rev_id")); - if (curCredRevId > credRevId) { - credRevId = curCredRevId; - credId = JsonPath.read(element, "$.cred_info.referent"); - } - } - else { // case of not support revocation - credId = JsonPath.read(element, "$.cred_info.referent"); - } - } - log.info("Use latest credential in demo - credId: "+ credId); - - // Make body using presentationRequest - LinkedHashMap reqAttrs = JsonPath.read(presentationRequest, "$.requested_attributes"); - for(String key : reqAttrs.keySet()) - reqAttrs.replace(key, JsonPath.parse("{ cred_id: '" + credId + "', revealed: true }").json()); - - LinkedHashMap reqPreds = JsonPath.read(presentationRequest, "$.requested_predicates"); - for(String key : reqPreds.keySet()) - reqPreds.replace(key, JsonPath.parse("{ cred_id: '" + credId + "' }").json()); - - LinkedHashMap selfAttrs = new LinkedHashMap<>(); - - String body = JsonPath.parse("{}").put("$", "requested_attributes", reqAttrs) - .put("$", "requested_predicates", reqPreds) - .put("$", "self_attested_attributes", selfAttrs).jsonString(); - - response = client.requestPOST(agentApiUrl + "/present-proof/records/" + presExId + "/send-presentation", accessToken, body); - log.info("response: " + response); - } - - public void showWebViewAndSelect(String content) { - try { - String webViewContent = JsonPath.parse((LinkedHashMap)JsonPath.read(content, "$.content")).jsonString(); - log.info("webViewContent: " + webViewContent); - String webViewUrl = JsonPath.read(webViewContent, "$.web_view_url"); - - // CODE HERE : Show web view to user and user select & submit a item - - // For automation, we submit a item directly - String[] token = webViewUrl.split("/web-view/form.html"); - String issuerUrl = token[0]; - String[] token2 = webViewUrl.split("connectionId="); - String connectionId = token2[1]; - - String body = JsonPath.parse("{" + - " connectionId: '" + connectionId + "'," + - " selectedItemId: 'item1Id'" + - "}").jsonString(); - - String response = client.requestPOST(issuerUrl + "/web-view/submit", "", body); - log.info("response: " + response); - } catch (PathNotFoundException e) { - log.warn("Invalid content format -> Ignore"); - } - } - - public void sendCredentialRequest(String credExId) { - String response = client.requestPOST(agentApiUrl + "/issue-credential/records/" + credExId + "/send-request", accessToken, "{}"); - log.info("response: " + response); - } - - public void delayedExit() { - TimerTask task = new TimerTask() { - public void run() { - log.info("Holder demo completes - Exit"); - System.exit(0); - } - }; - Timer timer = new Timer("Timer"); - timer.schedule(task, 100L); - } -} \ No newline at end of file diff --git a/src/main/java/com/sktelecom/initial/controller/holder_webhook/Application.java b/src/main/java/com/sktelecom/initial/controller/holder_webhook/Application.java new file mode 100644 index 0000000..30e58bf --- /dev/null +++ b/src/main/java/com/sktelecom/initial/controller/holder_webhook/Application.java @@ -0,0 +1,15 @@ +package com.sktelecom.initial.controller.holder_webhook; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@Slf4j +public class Application { + + public static void main(String[] args) { + SpringApplication.run(com.sktelecom.initial.controller.holder_webhook.Application.class, args); + // GlobalService.initializeAfterStartup() is automatically called after this application starts. + } +} diff --git a/src/main/java/com/sktelecom/initial/controller/holder_webhook/GlobalController.java b/src/main/java/com/sktelecom/initial/controller/holder_webhook/GlobalController.java new file mode 100644 index 0000000..d2d3461 --- /dev/null +++ b/src/main/java/com/sktelecom/initial/controller/holder_webhook/GlobalController.java @@ -0,0 +1,25 @@ +package com.sktelecom.initial.controller.holder_webhook; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@Slf4j +@RestController +public class GlobalController { + + @Autowired + GlobalService globalService; + + @PostMapping("/webhooks/topic/{topic}") + public ResponseEntity webhooksTopicHandler( + @PathVariable String topic, + @RequestBody String body) { + globalService.handleEvent(topic, body); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/com/sktelecom/initial/controller/holder_webhook/GlobalService.java b/src/main/java/com/sktelecom/initial/controller/holder_webhook/GlobalService.java new file mode 100644 index 0000000..26bdbe2 --- /dev/null +++ b/src/main/java/com/sktelecom/initial/controller/holder_webhook/GlobalService.java @@ -0,0 +1,300 @@ +package com.sktelecom.initial.controller.holder_webhook; + +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.PathNotFoundException; +import com.sktelecom.initial.controller.utils.HttpClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.LinkedHashMap; + +import static com.sktelecom.initial.controller.utils.Common.*; + +@RequiredArgsConstructor +@Service +@Slf4j +public class GlobalService { + private final HttpClient client = new HttpClient(); + + @Value("${agentApiUrl}") + private String agentApiUrl; // agent service api url + + @Value("${accessToken}") + private String accessToken; // controller access token + + @Value("${mobileIssuerInvitationUrl}") + private String mobileIssuerInvitationUrl; + + @Value("${mobileCredDefId}") + private String mobileCredDefId; + + @Value("${mobileCredProposalEncodeData}") + private String mobileCredProposalEncodeData; + + @Value("${mobileCredProposalSite}") + private String mobileCredProposalSite; + + @Value("${mobileCredProposalReqSeq}") + private String mobileCredProposalReqSeq; + + @Value("${tpIssuerInvitationUrl}") + private String tpIssuerInvitationUrl; + + @Value("${tpCredDefId}") + private String tpCredDefId; + + @Value("${tpVerifierInvitationUrl}") + private String tpVerifierInvitationUrl; + + @Value("${runType}") + private String runType; + + private boolean mobileCredentialReceived = false; + + @EventListener(ApplicationReadyEvent.class) + public void initializeAfterStartup() { + log.info("--- Preparation Process Start ---"); + log.info("Receive invitation from mobile credential issuer"); + receiveInvitation(mobileIssuerInvitationUrl); + } + + public void handleEvent(String topic, String body) { + String state = null; + try { + state = JsonPath.read(body, "$.state"); + } catch (PathNotFoundException e) {} + log.info("handleEvent >>> topic:" + topic + ", state:" + state + ", body:" + body); + + // Phase.1 - receive mobile credential + if (!mobileCredentialReceived) { + switch(topic) { + case "connections": + if (state.equals("active")) { // #1 + log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendCredentialProposal"); + String connectionId = JsonPath.read(body, "$.connection_id"); + sendMobileCredentialProposal(connectionId); + } + break; + case "issue_credential": + if (state.equals("offer_received")) { // #2 + log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendCredentialRequest"); + String credExId = JsonPath.read(body, "$.credential_exchange_id"); + sendCredentialRequest(credExId); + } + else if (state.equals("credential_acked")) { // #3 + log.info("- Case (topic:" + topic + ", state:" + state + ") -> mobile credential received"); + String credential = JsonPath.parse((LinkedHashMap)JsonPath.read(body, "$.credential")).jsonString(); + log.info("credential: " + credential); + log.info("--- Preparation Process End ---"); + mobileCredentialReceived = true; + if (runType.equals("issuer")) { + log.info("--- Issue Process Start ---"); + log.info("receive invitation from third party issuer"); + receiveInvitation(tpIssuerInvitationUrl); + } + else { + log.info("--- Verify Process Start ---"); + log.info("receive invitation from third party verifier"); + receiveInvitation(tpVerifierInvitationUrl); + } + } + else if (state.equals("abandoned")) { + log.info("- Case (topic:" + topic + ", state:" + state + ") -> Print Error Message"); + String errorMsg = JsonPath.read(body, "$.error_msg"); + log.warn(" - error_msg: " + errorMsg); + } + break; + case "present_proof": + case "basicmessages": + break; + default: + log.warn("- Warning Unexpected topic:" + topic); + } + } + // Phase.2-1 - receive third party credential + else if (runType.equals("issuer")) { + switch(topic) { + case "connections": + if (state.equals("active")) { // #1 + log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendTpCredentialProposal"); + String connectionId = JsonPath.read(body, "$.connection_id"); + sendTpCredentialProposal(connectionId); + } + break; + case "present_proof": + if (state.equals("request_received")) { // #2 + log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendMobilePresentation"); + String presExId = JsonPath.read(body, "$.presentation_exchange_id"); + String presentationRequest = JsonPath.parse((LinkedHashMap)JsonPath.read(body, "$.presentation_request")).jsonString(); + sendPresentation(presExId, presentationRequest); + } + else if (state.equals("abandoned")) { + log.info("- Case (topic:" + topic + ", state:" + state + ") -> Print Error Message"); + String errorMsg = JsonPath.read(body, "$.error_msg"); + log.warn(" - error_msg: " + errorMsg); + } + break; + case "issue_credential": + if (state.equals("offer_received")) { // #3 + log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendTpCredentialRequest"); + String credExId = JsonPath.read(body, "$.credential_exchange_id"); + sendCredentialRequest(credExId); + } + else if (state.equals("credential_acked")) { // #4 + log.info("- Case (topic:" + topic + ", state:" + state + ") -> third party credential received"); + String credential = JsonPath.parse((LinkedHashMap)JsonPath.read(body, "$.credential")).jsonString(); + log.info("credential: " + credential); + log.info("--- Issue Process End ---"); + log.info("issuer demo completed successfully"); + System.exit(0); + } + else if (state.equals("abandoned")) { + log.info("- Case (topic:" + topic + ", state:" + state + ") -> Print Error Message"); + String errorMsg = JsonPath.read(body, "$.error_msg"); + log.warn(" - error_msg: " + errorMsg); + } + break; + case "basicmessages": + break; + default: + log.warn("- Warning Unexpected topic:" + topic); + } + } + // Phase.2-2 - verify mobile credential + else if (runType.equals("verifier")) { + switch(topic) { + case "connections": + if (state.equals("active")) { // #1 + log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendMobilePresentationProposal"); + String connectionId = JsonPath.read(body, "$.connection_id"); + sendMobilePresentationProposal(connectionId); + } + break; + case "present_proof": + if (state.equals("request_received")) { // #2 + log.info("- Case (topic:" + topic + ", state:" + state + ") -> sendMobilePresentation"); + String presExId = JsonPath.read(body, "$.presentation_exchange_id"); + String presentationRequest = JsonPath.parse((LinkedHashMap)JsonPath.read(body, "$.presentation_request")).jsonString(); + sendPresentation(presExId, presentationRequest); + } + if (state.equals("presentation_acked")) { // #3 + log.info("- Case (topic:" + topic + ", state:" + state + ") -> mobile presentation acked"); + log.info("--- Verify Process End ---"); + log.info("verifier demo completed successfully"); + System.exit(0); + } + else if (state.equals("abandoned")) { + log.info("- Case (topic:" + topic + ", state:" + state + ") -> Print Error Message"); + String errorMsg = JsonPath.read(body, "$.error_msg"); + log.warn(" - error_msg: " + errorMsg); + } + break; + case "issue_credential": + case "basicmessages": + break; + default: + log.warn("- Warning Unexpected topic:" + topic); + } + } + } + + public void receiveInvitation(String invitationUrlApi) { + String invitationUrl = client.requestGET(invitationUrlApi, ""); + log.info("invitation-url: " + invitationUrl); + String invitation = parseInvitationUrl(invitationUrl); + if (invitation == null) { + log.warn("Invalid invitationUrl"); + return; + } + log.info("invitation: " + invitation); + String response = client.requestPOST(agentApiUrl + "/connections/receive-invitation", accessToken, invitation); + log.info("response: " + response); + } + + public void sendMobileCredentialProposal(String connectionId) { + String comment = JsonPath.parse("{" + + " site: '" + mobileCredProposalSite + "'," + + " req_seq: '" + mobileCredProposalReqSeq + "'," + + " encode_data: '" + mobileCredProposalEncodeData + "'," + + "}").jsonString(); + String body = JsonPath.parse("{" + + " connection_id: '" + connectionId + "'," + + " cred_def_id: '" + mobileCredDefId + "'," + + " comment: '" + comment + "'," + + "}").jsonString(); + String response = client.requestPOST(agentApiUrl + "/issue-credential/send-proposal", accessToken, body); + log.debug("response: " + response); + } + + public void sendCredentialRequest(String credExId) { + String response = client.requestPOST(agentApiUrl + "/issue-credential/records/" + credExId + "/send-request", accessToken, "{}"); + log.debug("response: " + response); + } + + public void sendTpCredentialProposal(String connectionId) { + String body = JsonPath.parse("{" + + " connection_id: '" + connectionId + "'," + + " cred_def_id: '" + tpCredDefId + "'," + + "}").jsonString(); + String response = client.requestPOST(agentApiUrl + "/issue-credential/send-proposal", accessToken, body); + log.debug("response: " + response); + } + + public void sendPresentation(String presExId, String presentationRequest) { + String response = client.requestGET(agentApiUrl + "/present-proof/records/" + presExId + "/credentials", accessToken); + log.info("Matching Credentials in my wallet: " + response); + + ArrayList> credentials = JsonPath.read(response, "$"); + int credRevId = 0; + String credId = null; + for (LinkedHashMap element : credentials) { + if (JsonPath.read(element, "$.cred_info.cred_rev_id") != null){ // case of support revocation + int curCredRevId = Integer.parseInt(JsonPath.read(element, "$.cred_info.cred_rev_id")); + if (curCredRevId > credRevId) { + credRevId = curCredRevId; + credId = JsonPath.read(element, "$.cred_info.referent"); + } + } + else { // case of not support revocation + credId = JsonPath.read(element, "$.cred_info.referent"); + } + } + log.info("Use latest credential in demo - credential_id: "+ credId); + + // Make body using presentationRequest + LinkedHashMap reqAttrs = JsonPath.read(presentationRequest, "$.requested_attributes"); + for(String key : reqAttrs.keySet()) + reqAttrs.replace(key, JsonPath.parse("{ cred_id: '" + credId + "', revealed: true }").json()); + + LinkedHashMap reqPreds = JsonPath.read(presentationRequest, "$.requested_predicates"); + for(String key : reqPreds.keySet()) + reqPreds.replace(key, JsonPath.parse("{ cred_id: '" + credId + "' }").json()); + + LinkedHashMap selfAttrs = new LinkedHashMap<>(); + + String body = JsonPath.parse("{}").put("$", "requested_attributes", reqAttrs) + .put("$", "requested_predicates", reqPreds) + .put("$", "self_attested_attributes", selfAttrs).jsonString(); + + response = client.requestPOST(agentApiUrl + "/present-proof/records/" + presExId + "/send-presentation", accessToken, body); + log.debug("response: " + response); + } + + public void sendMobilePresentationProposal(String connectionId) { + String body = JsonPath.parse("{" + + " connection_id: '" + connectionId + "'," + + " presentation_proposal: {" + + " attributes: []," + + " predicates: []" + + " }" + + "}").jsonString(); + String response = client.requestPOST(agentApiUrl + "/present-proof/send-proposal", accessToken, body); + log.debug("response: " + response); + } + +} diff --git a/src/main/java/com/sktelecom/initial/controller/issuer/GlobalController.java b/src/main/java/com/sktelecom/initial/controller/issuer/GlobalController.java index 70df49a..0b6575e 100644 --- a/src/main/java/com/sktelecom/initial/controller/issuer/GlobalController.java +++ b/src/main/java/com/sktelecom/initial/controller/issuer/GlobalController.java @@ -3,12 +3,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import static com.sktelecom.initial.controller.utils.Common.*; - @RequiredArgsConstructor @Slf4j @RestController @@ -17,23 +14,11 @@ public class GlobalController { @Autowired GlobalService globalService; - @GetMapping(value = "/invitation") - public String invitationHandler() { - String invitationUrl = globalService.createInvitationUrl(); - return parseInvitationUrl(invitationUrl); - } - @GetMapping(value = "/invitation-url") public String invitationUrlHandler() { return globalService.createInvitationUrl(); } - @GetMapping(value = "/invitation-qr", produces = MediaType.IMAGE_PNG_VALUE) - public byte[] getInvitationUrlQRCode() { - String invitationUrl = globalService.createInvitationUrl(); - return generateQRCode(invitationUrl, 300, 300); - } - @PostMapping(value = "/webhooks") public ResponseEntity webhooksTopicHandler(@RequestBody String body) { globalService.handleEvent(body); diff --git a/src/main/java/com/sktelecom/initial/controller/issuer/GlobalService.java b/src/main/java/com/sktelecom/initial/controller/issuer/GlobalService.java index dbce3f0..08e55a2 100644 --- a/src/main/java/com/sktelecom/initial/controller/issuer/GlobalService.java +++ b/src/main/java/com/sktelecom/initial/controller/issuer/GlobalService.java @@ -2,6 +2,7 @@ import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; +import com.sktelecom.initial.controller.utils.Aes256Util; import com.sktelecom.initial.controller.utils.HttpClient; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -35,9 +36,19 @@ public class GlobalService { @Value("${webViewUrl}") private String webViewUrl; // web view form url + @Value("${isEncrypted}") + private Boolean isEncrypted; // encryption support + + @Value("${cipherKey}") + private String cipherKey; // cipherKey to decrypt + + @Value("${cipherIvKey}") + private String cipherIvKey; // cipherIvKey to decrypt + String orgName; String orgImageUrl; String publicDid; + boolean webhookUrlIsValid = false; LinkedHashMap connIdToCredExId = new LinkedHashMap<>(); // cache to keep credential issuing flow @@ -64,70 +75,108 @@ public void initializeAfterStartup() { } public void handleEvent(String body) { + if (!webhookUrlIsValid) + webhookUrlIsValid = true; + + if (isEncrypted) { + Aes256Util aes256Util = new Aes256Util(); + String encodeData = JsonPath.read(body, "$.encode_data"); + try { + body = aes256Util.decrypt(cipherKey, cipherIvKey, encodeData); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + String topic = JsonPath.read(body, "$.topic"); - String state = topic.equals("problem_report") ? null : JsonPath.read(body, "$.state"); + String state = JsonPath.read(body, "$.state"); log.info("handleEvent >>> topic:" + topic + ", state:" + state + ", body:" + body); switch(topic) { case "issue_credential": - // 1. holder 가 credential 을 요청함 -> 개인정보이용 동의 요청 + // 1. holder 가 credential 을 요청함 -> 모바일 가입증명 검증 요청 if (state.equals("proposal_received")) { - log.info("- Case (topic:" + topic + ", state:" + state + ") -> checkCredentialProposal && sendAgreement"); - if(checkCredentialProposal(body)) { - sendAgreement(JsonPath.read(body, "$.connection_id")); + log.info("- Case (topic:" + topic + ", state:" + state + ") -> checkCredentialProposal && sendPresentationRequest"); + String connectionId = JsonPath.read(body, "$.connection_id"); + String credExId = JsonPath.read(body, "$.credential_exchange_id"); + String credentialProposal = JsonPath.parse((LinkedHashMap)JsonPath.read(body, "$.credential_proposal_dict")).jsonString(); + if(checkCredentialProposal(connectionId, credExId, credentialProposal)) { + /** + * Send Presentation Request API + * Guide : https://initial-v2-platform.readthedocs.io/ko/master/open_api_proof/#step-1-mandatory-verifier-holder-verification-request + * Swagger : https://app.swaggerhub.com/apis-docs/khujin1/initial_Cloud_Agent_Open_API/1.0.4#/present-proof%20v1.0/post_present_proof_send_verification_request + */ + sendPresentationRequest(connectionId); } } - // 4. holder 가 증명서를 정상 저장하였음 -> 완료 (revocation 은 아래 코드 참조) + // 3. holder 가 증명서를 정상 저장하였음 -> 완료 else if (state.equals("credential_acked")) { log.info("- Case (topic:" + topic + ", state:" + state + ") -> credential issued successfully"); // TODO: should store credential_exchange_id to revoke this credential // connIdToCredExId is simple example for this if (enableRevoke) { + log.info("Revoke is enabled -> revokeCredential"); String connectionId = JsonPath.read(body, "$.connection_id"); String credExId = connIdToCredExId.get(connectionId); + /** + * Revocation Credential Request API + * Guide : https://initial-v2-platform.readthedocs.io/ko/master/open_api_revocation/ + * Swagger : https://app.swaggerhub.com/apis-docs/khujin1/initial_Cloud_Agent_Open_API/1.0.4#/revocation/post_revocation_revoke + */ revokeCredential(credExId); } } - break; - case "basicmessages": - String content = JsonPath.read(body, "$.content"); - String type = getTypeFromBasicMessage(content); - // 2. holder 가 개인정보이용 동의를 보냄 -> 모바일 가입증명 검증 요청 - if (type != null && type.equals("initial_agreement_decision")) { - if (isAgreementAgreed(content)) { - log.info("- Case (topic:" + topic + ", state:" + state + ", type:" + type + ") -> AgreementAgreed & sendPresentationRequest"); - sendPresentationRequest(JsonPath.read(body, "$.connection_id")); - } + else if (state.equals("abandoned")) { + log.info("- Case (topic:" + topic + ", state:" + state + ") -> Print Error Message"); + String errorMsg = JsonPath.read(body, "$.error_msg"); + log.warn(" - error_msg: " + errorMsg); } - else - log.warn("- Warning: Unexpected type:" + type); break; case "present_proof": - // 3. holder 가 보낸 모바일 가입증명 검증 완료 + // 2. holder 가 보낸 모바일 가입증명 검증 완료 -> 증명서 발행 if (state.equals("verified")) { log.info("- Case (topic:" + topic + ", state:" + state + ") -> getPresentationResult"); LinkedHashMap attrs = getPresentationResult(body); - for(String key : attrs.keySet()) - log.info("Requested Attribute - " + key + ": " + attrs.get(key)); if (enableWebView) { - // 3-1. 검증 값 정보로 발행할 증명서가 한정되지 않는 경우 추가 정보 요구 + // 2-1. 검증 값 정보로 발행할 증명서가 한정되지 않는 경우 추가 정보 요구 log.info("Web View enabled -> sendWebView"); - sendWebView(JsonPath.read(body, "$.connection_id"), attrs, body); + String connectionId = JsonPath.read(body, "$.connection_id"); + /** + * Send Message Request API + * Guide : https://initial-v2-platform.readthedocs.io/ko/master/open_api_message/#1-webview-spec + * Swagger : https://app.swaggerhub.com/apis-docs/khujin1/initial_Cloud_Agent_Open_API/1.0.4#/basicmessage/post_connections__conn_id__send_message + */ + sendWebView(connectionId, attrs, body); } else { - // 3-2. 검증 값 정보 만으로 발행할 증명서가 한정되는 경우 증명서 바로 발행 + // 2-2. 검증 값 정보 만으로 발행할 증명서가 한정되는 경우 증명서 바로 발행 log.info("Web View is not used -> sendCredentialOffer"); - sendCredentialOffer(JsonPath.read(body, "$.connection_id"), attrs, null); + String connectionId = JsonPath.read(body, "$.connection_id"); + /** + * Send Credential Offer Request API + * Guide : https://initial-v2-platform.readthedocs.io/ko/master/open_api_auto_credential/#step-1-mandatory-holder + * Swagger : https://app.swaggerhub.com/apis-docs/khujin1/initial_Cloud_Agent_Open_API/1.0.4#/issue-credential%20v1.0/post_issue_credential_records__cred_ex_id__send_offer + */ + if (isEncrypted) + sendCredentialOfferEnc(connectionId, attrs, null); + else + sendCredentialOffer(connectionId, attrs, null); } } + else if (state.equals("abandoned")) { + log.info("- Case (topic:" + topic + ", state:" + state + ") -> Print Error Message"); + String errorMsg = JsonPath.read(body, "$.error_msg"); + log.warn(" - error_msg: " + errorMsg); + } break; case "problem_report": log.warn("- Case (topic:" + topic + ") -> Print body"); - log.warn(" - body:" + prettyJson(body)); + log.warn(" - body:" + body); break; case "connections": + case "basicmessages": case "revocation_registry": case "issuer_cred_rev": break; @@ -182,6 +231,19 @@ void provisionController() { } log.info("- SUCCESS : " + verifTplId + " exists"); } + + log.info("TEST - Check if webhook url (in console) is valid"); + // create non-public invitation to receive webhook message + client.requestPOST(agentApiUrl + "/connections/create-invitation", accessToken, "{}"); + try { + Thread.sleep(1000); // wait to receive webhook message + } catch (InterruptedException e) {} + if (!webhookUrlIsValid) { + log.info("- FAILED: webhook message is not received - Check if it is valid in console configuration"); + System.exit(0); + } + log.info("- SUCCESS"); + } public String createInvitationUrl() { @@ -195,59 +257,152 @@ public String createInvitationUrl() { } } - public boolean checkCredentialProposal(String credExRecord) { - String credentialProposal = JsonPath.parse((LinkedHashMap)JsonPath.read(credExRecord, "$.credential_proposal_dict")).jsonString(); + public void sendCredProblemReport(String credExId, String description) { + String body = JsonPath.parse("{" + + " description: '" + description + "'" + + "}").jsonString(); + String response = client.requestPOST(agentApiUrl + "/issue-credential/records/" + credExId + "/problem-report", accessToken, body); + log.info("response: " + response); + } + + public boolean checkCredentialProposal(String connectionId, String credExId, String credentialProposal) { try { String requestedCredDefId = JsonPath.read(credentialProposal, "$.cred_def_id"); if (requestedCredDefId.equals(credDefId)){ - String connectionId = JsonPath.read(credExRecord, "$.connection_id"); - String credExId = JsonPath.read(credExRecord, "$.credential_exchange_id"); connIdToCredExId.put(connectionId, credExId); return true; } log.warn("This issuer can issue credDefId:" + credDefId); - log.warn("But, requested credDefId is " + requestedCredDefId + " -> Ignore"); + log.warn("But, requested credDefId is " + requestedCredDefId + " -> problemReport"); + sendCredProblemReport(credExId, "본 기관은 요청한 증명서 (credDefId:" + requestedCredDefId + ") 를 발급하지 않습니다"); } catch (PathNotFoundException e) { - log.warn("Requested credDefId does not exist -> Ignore"); + log.warn("Requested credDefId does not exist -> problemReport"); + sendCredProblemReport(credExId, "증명서 (credDefId) 가 지정되지 않았습니다"); } return false; } - public void sendAgreement(String connectionId) { - String initialAgreement = JsonPath.parse("{" + - " type : 'initial_agreement',"+ - " content: {" + - " title : '개인정보 수집 및 이용 동의서'," + - " agreement: 'initial 서비스(이하 서비스라 한다)와 관련하여, 본인은 동의내용을 숙지하였으며, 이에 따라 본인의 개인정보를 (주)XXXX가 수집 및 이용하는 것에 대해 동의합니다.\n\n본 동의는 서비스의 본질적 기능 제공을 위한 개인정보 수집/이용에 대한 동의로서, 동의를 하는 경우에만 서비스 이용이 가능합니다.\n\n법령에 따른 개인정보의 수집/이용, 계약의 이행/편익제공을 위한 개인정보 취급위탁 및 개인정보 취급과 관련된 일반 사항은 서비스의 개인정보 처리방침에 따릅니다.'," + - " collectiontype: '이름,생년월일'," + - " usagepurpose: '서비스 이용에 따른 본인확인'," + - " consentperiod : '1년',"+ - " }"+ + public void sendPresentationRequest(String connectionId) { + // sample agreement + String agreement = JsonPath.parse("{" + + " type: 'initial_agreement',"+ + " content: [" + + " {" + + " sequence: 1," + + " title: '개인정보 수집 및 이용 동의서'," + + " is_mandatory: 'true'," + + " terms_id: 'person'," + + " terms_ver: '1.0'," + + " agreement: 'initial서비스(이하“서비스”라 한다)와 관련하여, 본인은 동의 내용을 숙지하였으며, 이에 따라 본인의 개인정보를 귀사(SK텔레콤주식회사)가 수집 및 이용하는 것에 대해 동의 합니다. 본 동의는 서비스의 본질적 기능 제공을 위한 개인정보 수집/이용에 대한 동의로서, 동의를 하는 경우에만 서비스 이용이 가능합니다. 법령에 따른 개인정보의 수집/이용, 계약의 이행/편익 제공을 위한 개인정보 취급 위탁 및 개인정보 취급과 관련된 일반 사항은 서비스의 개인정보 처리 방침에 따릅니다.'," + + " condition: [" + + " {" + + " sub_title: '수집 항목'," + + " target: '이름,생년월일'" + + " }," + + " {" + + " sub_title: '수집 및 이용목적'," + + " target: '서비스 이용에 따른 본인확인'" + + " }," + + " {" + + " sub_title: '이용기간 및 보유/파기'," + + " target: '1년'" + + " }" + + " ]" + + " }," + + " {" + + " sequence: 2," + + " title: '위치정보 수집 및 이용 동의서'," + + " is_mandatory: 'true'," + + " terms_id: 'location'," + + " terms_ver: '1.0'," + + " agreement: '이 약관은 이니셜(SK텔레콤)(이하“회사”)가 제공하는 위치 정보사업 또는 위치기반 서비스 사업과 관련하여 회사와 개인 위치 정보주체와의 권리, 의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.'," + + " condition: [" + + " {" + + " sub_title: '위치정보 수집 방법'," + + " target: 'GPS칩'" + + " }," + + " {" + + " sub_title: '위치정보 이용/제공'," + + " target: '이 약관에 명시되지 않은 사항은 위치정보의 보호 및 이용 등에 관한 법률, 정보통신망 이용촉진 및 정보보호 등에 관한 법률, 전기통신기본법, 전기통신사업법 등 관계법령과 회사의 이용약관 및 개인정보취급방침, 회사가 별도로 정한 지침 등에 의합니다.'" + + " }," + + " {" + + " sub_title: '수집목적'," + + " target: '현재의 위치를 기반으로 하여 주변 매장의 위치 등의 정보를 제공하는 서비스'" + + " }," + + " {" + + " sub_title: '위치정보 보유기간'," + + " target: '1년'" + + " }" + + " ]" + + " }," + + " {" + + " sequence: 3," + + " title: '제3자 정보제공 동의서'," + + " is_mandatory: 'true'," + + " terms_id: '3rdparty'," + + " terms_ver: '1.0'," + + " agreement: 'initial서비스(이하“서비스”라 한다)와 관련하여, 본인은 동의 내용을 숙지하였으며, 이에 따라 본인의 개인정보를 귀사(이슈어)가 수집한 개인정보를 아래와 같이 제3자에게 제공하는 것에 대해 동의 합니다. 고객은 개인정보의 제3자 제공에 대한 동의를 거부할 권리가 있으며, 동의를 거부할 시 받는 별도의 불이익은 없습니다. 단, 서비스 이용이 불가능하거나, 서비스 이용 목적에 따른 서비스 제공에 제한이 따르게 됩니다.'," + + " condition: [" + + " {" + + " sub_title: '제공하는 자'," + + " target: '발급기관'" + + " }," + + " {" + + " sub_title: '제공받는 자'," + + " target: '이니셜(SK텔레콤)'" + + " }," + + " {" + + " sub_title: '제공받는 항목'," + + " target: '생년월일,시험일,성명(영문),만료일,성명(한글),수험번호,듣기점수,읽기점수,총점'" + + " }," + + " {" + + " sub_title: '수집 및 이용목적'," + + " target: '모바일 전자증명서 발급'" + + " }," + + " {" + + " sub_title: '보유 및 이용기간'," + + " target: '모바일 전자증명서 발급을 위해 서버에 임시 저장하였다가, 증명서 발행 후 즉시 삭제(단, 고객 단말기 내부 저장영역에 증명서 형태로 저장/보관)'" + + " }" + + " ]" + + " }" + + " ]"+ "}").jsonString(); - String body = JsonPath.parse("{ content: '" + initialAgreement + "' }").jsonString(); - String response = client.requestPOST(agentApiUrl + "/connections/" + connectionId + "/send-message", accessToken, body); - log.info("response: " + response); - } - - public boolean isAgreementAgreed(String content) { - try { - String decisionContent = JsonPath.parse((LinkedHashMap)JsonPath.read(content, "$.content")).jsonString(); - log.info("decisionContent: " + decisionContent); - String agree = JsonPath.read(decisionContent, "$.agree_yn"); - if (agree.equals("Y")) - return true; - log.warn("Agreement is not Agreed -> Ignore"); - } catch (PathNotFoundException e) { - log.warn("Invalid content format -> Ignore"); - } - - return false; - } + String selfAttestedAttrs = JsonPath.parse("[" + + "{"+ + " type: 'hint',"+ + " content: [" + + " {" + + " attr: 'school_id'," + + " hintText: '학번을 입력해주세요.'," + + " tooltip: {" + + " title: '동물등록정보를 잊어버리셨나요?'," + + " content: '동물보호관리시스템(https://www.animal.go.kr)'," + + " linkButton: {" + + " text: '자세히보기'," + + " url: 'https://www.animal.go.kr'" + + " }" + + " }" + + " }," + + " {" + + " attr: 'date_of_birth'," + + " hintText: '학번을 입력해주세요.'," + + " tooltip: {" + + " title: '동물등록정보를 잊어버리셨나요?'," + + " content: '동물보호관리시스템(https://www.animal.go.kr)'," + + " linkButton: {" + + " text: '자세히보기'," + + " url: 'https://www.animal.go.kr'" + + " }" + + " }" + + " }" + + " ]" + + "}]").jsonString(); - public void sendPresentationRequest(String connectionId) { String body = JsonPath.parse("{" + " connection_id: '" + connectionId + "'," + - " verification_template_id: '" + verifTplId + "'" + + " verification_template_id: '" + verifTplId + "'," + + " agreement: " + agreement + "," + + " self_attested_attrs: " + selfAttestedAttrs + "," + "}").jsonString(); String response = client.requestPOST(agentApiUrl + "/present-proof/send-verification-request", accessToken, body); log.info("response: " + response); @@ -257,12 +412,22 @@ public LinkedHashMap getPresentationResult(String presExRecord) String verified = JsonPath.read(presExRecord, "$.verified"); if (!verified.equals("true")) { log.info("proof is not verified"); + log.info("Possible Reason: Revoked or Signature mismatch or Predicates unsatisfied"); return null; } + String requestedProof = JsonPath.parse((LinkedHashMap)JsonPath.read(presExRecord, "$.presentation.requested_proof")).jsonString(); + + LinkedHashMap revealedAttrs = JsonPath.read(requestedProof, "$.revealed_attrs"); LinkedHashMap attrs = new LinkedHashMap<>(); - LinkedHashMap revealedAttrs = JsonPath.read(presExRecord, "$.presentation.requested_proof.revealed_attrs"); for(String key : revealedAttrs.keySet()) attrs.put(key, JsonPath.read(revealedAttrs.get(key), "$.raw")); + for(String key : attrs.keySet()) + log.info("Requested Attribute - " + key + ": " + attrs.get(key)); + + LinkedHashMap predicates = JsonPath.read(requestedProof, "$.predicates"); + for(String key : predicates.keySet()) + log.info("Requested Predicates - " + key + " is satisfied"); + return attrs; } @@ -283,7 +448,7 @@ public void sendCredentialOffer(String connectionId, LinkedHashMap attrs, String selectedItemId) { + // TODO: need to implement business logic to query information for holder + // we assume that the value is obtained by querying DB (e.g., attrs.mobileNum and selectedItemId) + LinkedHashMap value = new LinkedHashMap<>(); + value.put("name", "김증명"); + value.put("date", "20180228"); + value.put("degree", "컴퓨터공학"); + value.put("age", "25"); + value.put("photo", "JpegImageBase64EncodedBinary"); + + // value insertion + String body = JsonPath.parse("{" + + " counter_proposal: {" + + " cred_def_id: '" + credDefId + "'," + + " credential_proposal: {" + + " attributes: [" + + " { name: 'name', value: '" + value.get("name") + "' }," + + " { name: 'date', value: '" + value.get("date") + "' }," + + " { name: 'degree', value: '" + value.get("degree") + "' }," + + " { name: 'age', value: '" + value.get("age") + "' }," + + " { name: 'photo', value: '" + value.get("photo") + "' }" + + " ]" + + " }" + + " }" + + "}").jsonString(); + Aes256Util aes256Util = new Aes256Util(); + String encodeData; + try { + encodeData = aes256Util.encrypt(cipherKey, cipherIvKey, body); + } catch (Exception e) { + throw new RuntimeException(e); + } + String encBody = JsonPath.parse("{" + + " encode_data: '" + encodeData + "'" + + "}").jsonString(); + String credExId = connIdToCredExId.get(connectionId); + String response = client.requestPOST(agentApiUrl + "/enc/issue-credential/records/" + credExId + "/send-offer", accessToken, encBody); + log.info("response: " + response); + + String resBody; + try { + resBody = aes256Util.decrypt(cipherKey, cipherIvKey, JsonPath.read(response, "$.encode_data")); + } catch (Exception e) { + throw new RuntimeException(e); + } + log.info("resBody: " + resBody); + } + public void sendWebView(String connectionId, LinkedHashMap attrs, String presExRecord) { // TODO: need to implement business logic to query information for holder and prepare web view // we send web view form page (GET webViewUrl?connectionId={connectionId}) to holder in order to select a item by user @@ -320,7 +533,10 @@ public void handleWebView(String body) { // 3-1-1. 추가 정보 기반으로 증명서 발행 log.info("sendCredentialOffer with connectionId:" + connectionId + ", selectedItemId:" + selectedItemId); - sendCredentialOffer(connectionId, null, selectedItemId); + if (isEncrypted) + sendCredentialOfferEnc(connectionId, null, selectedItemId); + else + sendCredentialOffer(connectionId, null, selectedItemId); } public void revokeCredential(String credExId) { @@ -332,4 +548,4 @@ public void revokeCredential(String credExId) { String response = client.requestPOST(agentApiUrl + "/revocation/revoke", accessToken, body); log.info("response: " + response); } -} \ No newline at end of file +} diff --git a/src/main/java/com/sktelecom/initial/controller/utils/Aes256Util.java b/src/main/java/com/sktelecom/initial/controller/utils/Aes256Util.java new file mode 100644 index 0000000..d5ff0e1 --- /dev/null +++ b/src/main/java/com/sktelecom/initial/controller/utils/Aes256Util.java @@ -0,0 +1,113 @@ +package com.sktelecom.initial.controller.utils; + +import lombok.extern.slf4j.Slf4j; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; + +@Slf4j +public class Aes256Util { + + private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; //AES transformation + + /** + * 컨텐츠를 AES256 암호화하는 메소드. + * + * @param key AES 키 + * @param initialVector AES 초기화벡터 + * @param contents 암호화할 컨텐츠 + * @return contents AES256 암호화한 문자열 + * @throws Exception + */ + public String encrypt(String key, String initialVector, String contents) throws Exception { + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + + SecretKeySpec secretKeySpec = new SecretKeySpec(hexToBytes(key), "AES"); + IvParameterSpec ivParameterSpec = new IvParameterSpec(hexToBytes(initialVector)); + + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + + byte[] encrypted = cipher.doFinal(contents.getBytes("UTF-8")); + + return Base64.getEncoder().encodeToString(encrypted); + + } + + /** + * 암호화된 문자열을 원본 문자열로 복호화하는 메소드. + * + * @param key AES 키 + * @param initialVector AES 초기화벡터 + * @param contents 복호화할 컨텐츠 + * @return decryptStr 복호화된 문자열 + * @throws Exception + */ + public String decrypt(String key, String initialVector, String contents) throws Exception { + + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + + SecretKeySpec secretKeySpec = new SecretKeySpec(hexToBytes(key), "AES"); + IvParameterSpec ivParameterSpec = new IvParameterSpec(hexToBytes(initialVector)); + + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + + byte[] decodedBytes = Base64.getDecoder().decode(contents); + byte[] decrypted = cipher.doFinal(decodedBytes); + + return new String(decrypted, "UTF-8"); + + } + + /** + * 16진수 문자열을 바이트 배열로 변환하는 메소드 + * + * @param hex 16진수 문자열 + * @return 바이트 배열 + */ + private byte[] hexToBytes(String hex) { + byte[] b; + + if(hex == null) { + return null; + } + + int len = hex.length(); + if(len%2 == 1) { + return null; + } + + b = new byte[len / 2]; + + for(int i = 0; i < len; i += 2) { + b[i >> 1] = (byte)Integer.parseInt(hex.substring(i, i + 2), 16); + } + + return b; + } + +// public static void main(String[] args) throws Exception { +// +// try { +// +// String str = "{\"results\": [{\"referent\": \"dd95828f-2055-4548-99b9-9d881a80cfbd\", \"schema_id\": \"N6r4nLwAkcYUX8c8Kb8Ufu:2:CertificateOfTOEIC:4.0\", \"cred_def_id\": \"DrLbXFSao4Vo8gMfjxPxU1:3:CL:1617698238:81df0010-62b4-45b1-bd00-8d0ad74762fd\", \"rev_reg_id\": \"DrLbXFSao4Vo8gMfjxPxU1:4:DrLbXFSao4Vo8gMfjxPxU1:3:CL:1617698238:81df0010-62b4-45b1-bd00-8d0ad74762fd:CL_ACCUM:ba0c3ff0-00c0-40dc-8ee6-d6a1916542cf\", \"cred_rev_id\": \"11\", \"attrs\": {\"score_of_reading\": \"\", \"registration_number\": \"123456789-987654321\", \"exp_date\": \"20230228\", \"score_of_total\": \"990\", \"score_of_listening\": \"117\", \"date_of_birth\": \"19901001\", \"date_of_test\": \"20190105\", \"english_name\": \"ethan\", \"korean_name\": \"\\uae40\\uc99d\\uba85\"}}, {\"referent\": \"0922d433-c478-461f-a9e0-2b17b4853573\", \"schema_id\": \"cU8rErjgKj8fgn1kTDren:2:PersonIdentityCredential:1.0\", \"cred_def_id\": \"TmisnEAGBPeVVDjtAXPdYt:3:CL:0:v01\", \"rev_reg_id\": null, \"cred_rev_id\": null, \"attrs\": {\"mobile_num\": \"01023456789\", \"person_name\": \"\\uae40\\uc99d\\uba85\", \"telecom\": \"SKT\", \"date_of_birth\": \"19901001\", \"exp_date\": \"20230627\", \"gender\": \"M\", \"ci\": \"\", \"is_foreigner\": \"N\"}}]}"; +// +// Aes256Util aes256Util = new Aes256Util(); +// +// String encode = aes256Util.encrypt("9c91204af6be527d2a04d3599eba4176", "1aa0c49d92ce06a12d6916a2582b3c58", str); +// System.out.println(encode); +// String plain = aes256Util.decrypt("9c91204af6be527d2a04d3599eba4176", "1aa0c49d92ce06a12d6916a2582b3c58", encode); +// System.out.println(plain); +// +// String testStr = "zRTmpLMkF/sOcoJkbMrpjIpumI7zJymo5v4Dy1LEmfCcDB4mWOfsyiECdSqL81jdgwlkF2y+zrqi+qIs4ad4U5tdVUdLBALeRTHfwKZntCW0QBRTFFzq66lpW+1asPF69bJl1ns/Hrj4K2wQqSWMAOK2oFKzJA/85AKfDcidI+T0STZP4xc6sTuZRP3IOFOAPcxR9bqNTGrl8WHVWyBP7K9cCZSVbv8O6K8kBTtDgqxWJQkMSv7QUT1CcFKgkexz+5QzVPshHyel9nuPcMyMbp6OsLmoa/kweIpNVmloX6DPuPLlnoMM4K20qEbcGH2OaFKdHy/cRF7FUDC6OaCWB2YDpolYxeFfCKpiR6RdQ7h+z4uFFTfxUUgn5ek2n6ORy++Y4Rg+NgRnd/0EL8V0su0lEytbGVNVrzL7O2ges5UXryXV1KaRHXpl4MJWo9M/Jd5KWd0FfVOzOslx0tE5ya1/koGlld9OkDkkkOHb4Z84DMZnjU4PHTzGGBspLu98UJS62JdFTkm//gw3w1W7YZ5Xwag786w3hq22I66Jb2HGsoOYJ3HvzjmGZm08AatoceUpJTJKXTKfvtSz/OYC3JiEpjnkoAhu5VE/sHeG88AoZ06tf8qIGORxq01KiYqbu2iwjlMC0eMtJPpzkh0ufXAsbYd1mgN2AJzk04eOCFulbyq3iFJycpiaZVxXkyVyR8YbHpZN7iEHprDCfeQFVBdqjhezCq0R+TJTWGFSGFIfMqUNN3yz5bF+d212TY6l6eGaOGnDPSkBFcUIL4OD/VS6vnZMxaM3Ynta8XwJO05Ro4H06h95tr77sjrvt+1JF42WZE9xzv/nVngeFEslbDgAEqHDOwOF7Okwv2Q+L2zJFI2m+HOGewr4Y4E3a051HZBK35oYsnr+q+kQY/mBefK+MzghBdytbyLR+pikj446YrAa0sFkLofi3ttYOmHKmD6wx/rca1Jn3TsDELaPkxDj4RJWmEFh1pIJCJFVuZdcFc5cUKGOmLeKRydtCB4TXDrFkxbrRhMWacTliQabUZms7n/ijRnUDcMr8dmQxuJjQDkIAo1NrmJNz6b+bFf48U/Xcb7KvDyTB7MvPKofOP6WzxWj8SViIwzV6OPD0M+Bo8iweb176tC9/bC/Ih7S8Xaq4PINyEXhomtRkcpfbD2/LrgWz22Fvo6bE8MxGXKV0SmAUA5Qk5tYATcxH71XRxs6Onj87GynVHmjpTBqBM445i1j/jVDrtIgzpspZnZ8VhGwet2mRZzZEfEwhsav5F09x5/onJ1NxzmzzcUUgGoaTMXa38pg37BofxlnF+fOEobm5tE7Yigp+sOO6aXOfCJSgX/L7TSuns8JEzMPXAGCeVftlojB+4ZKhZ35hO+DCIUbLGE4ko9tKJNj3SSTY6BRAj1ezZYSg2pPpcHdg6H+cgNQV5R9XMhvPt0PZu65LVoxbxBlw/ZRq2BrPY1s"; +// String testStrResult = aes256Util.decrypt("9c91204af6be527d2a04d3599eba4176", "1aa0c49d92ce06a12d6916a2582b3c58", testStr); +// System.out.println(testStrResult); +// +// } catch (Exception e) { +// System.out.println(e.getMessage()); +// } +// } + +} diff --git a/src/main/java/com/sktelecom/initial/controller/utils/Common.java b/src/main/java/com/sktelecom/initial/controller/utils/Common.java index fbee8b9..dab8dfb 100644 --- a/src/main/java/com/sktelecom/initial/controller/utils/Common.java +++ b/src/main/java/com/sktelecom/initial/controller/utils/Common.java @@ -1,43 +1,14 @@ package com.sktelecom.initial.controller.utils; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParser; -import com.google.zxing.BarcodeFormat; -import com.google.zxing.MultiFormatWriter; -import com.google.zxing.WriterException; -import com.google.zxing.client.j2se.MatrixToImageConfig; -import com.google.zxing.client.j2se.MatrixToImageWriter; -import com.google.zxing.common.BitMatrix; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Base64; -import org.apache.commons.io.FileUtils; -import org.springframework.http.MediaType; -import org.springframework.util.Assert; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.ThreadLocalRandom; @RequiredArgsConstructor @Slf4j public class Common { - public static String prettyJson(String jsonString) { - Gson gson = new GsonBuilder().setPrettyPrinting().create(); - return gson.toJson(JsonParser.parseString(jsonString)); - } - - public static int getRandomInt(int min, int max) { - if (min >= max) - throw new IllegalArgumentException("max must be greater than min"); - return ThreadLocalRandom.current().nextInt(min, max); - } - public static String parseInvitationUrl(String invitationUrl) { String[] tokens = invitationUrl.split("\\?c_i="); if (tokens.length != 2) @@ -47,22 +18,6 @@ public static String parseInvitationUrl(String invitationUrl) { return new String(Base64.decodeBase64(encodedInvitation)); } - public static byte[] generateQRCode(String text, int width, int height) { - - Assert.hasText(text, "text must not be empty"); - Assert.isTrue(width > 0, "width must be greater than zero"); - Assert.isTrue(height > 0, "height must be greater than zero"); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try { - BitMatrix matrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, width, height); - MatrixToImageWriter.writeToStream(matrix, MediaType.IMAGE_PNG.getSubtype(), outputStream, new MatrixToImageConfig()); - } catch (IOException | WriterException e) { - e.printStackTrace(); - } - return outputStream.toByteArray(); - } - public static String getTypeFromBasicMessage(String content) { try { // return type @@ -71,9 +26,11 @@ public static String getTypeFromBasicMessage(String content) { return null; } - public static String encodeFileToBase64Binary(String fileName) throws IOException { - File file = new File(fileName); - byte[] encoded = Base64.encodeBase64(FileUtils.readFileToByteArray(file)); - return new String(encoded, StandardCharsets.US_ASCII); + public static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } } } diff --git a/src/main/java/com/sktelecom/initial/controller/utils/HttpClient.java b/src/main/java/com/sktelecom/initial/controller/utils/HttpClient.java index 96a9694..626bd49 100644 --- a/src/main/java/com/sktelecom/initial/controller/utils/HttpClient.java +++ b/src/main/java/com/sktelecom/initial/controller/utils/HttpClient.java @@ -38,10 +38,11 @@ public String requestPOST(String url, String token, String json) { return raw(request); } - public String requestPOSTBasicAuth(String url, String username, String password, String json) { + public String requestPOSTBasicAuth(String url, String username, String password, String form) { Request request = new Request.Builder() .url(url) - .post(RequestBody.create(json, JSON_TYPE)) + .post(RequestBody.create(form, MediaType.parse("application/x-www-form-urlencoded"))) + .addHeader("Content-Type", "application/x-www-form-urlencoded") .build(); request = addBasic(request, username, password); return raw(request); diff --git a/src/main/java/com/sktelecom/initial/controller/verifier/Application.java b/src/main/java/com/sktelecom/initial/controller/verifier/Application.java new file mode 100644 index 0000000..a116507 --- /dev/null +++ b/src/main/java/com/sktelecom/initial/controller/verifier/Application.java @@ -0,0 +1,15 @@ +package com.sktelecom.initial.controller.verifier; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@Slf4j +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + // GlobalService.initializeAfterStartup() is automatically called after this application starts. + } +} diff --git a/src/main/java/com/sktelecom/initial/controller/holder/GlobalController.java b/src/main/java/com/sktelecom/initial/controller/verifier/GlobalController.java similarity index 65% rename from src/main/java/com/sktelecom/initial/controller/holder/GlobalController.java rename to src/main/java/com/sktelecom/initial/controller/verifier/GlobalController.java index cf8a5ba..ebd185e 100644 --- a/src/main/java/com/sktelecom/initial/controller/holder/GlobalController.java +++ b/src/main/java/com/sktelecom/initial/controller/verifier/GlobalController.java @@ -1,10 +1,10 @@ -package com.sktelecom.initial.controller.holder; +package com.sktelecom.initial.controller.verifier; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -17,12 +17,14 @@ public class GlobalController { @Autowired GlobalService globalService; - @PostMapping("/webhooks") + @GetMapping(value = "/invitation-url") + public String invitationUrlHandler() { + return globalService.createInvitationUrl(); + } + + @PostMapping(value = "/webhooks") public ResponseEntity webhooksTopicHandler(@RequestBody String body) { - if (globalService.phase.equals("preparation")) - globalService.handleEventOnPreparation(body); - else - globalService.handleEvent(body); + globalService.handleEvent(body); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/sktelecom/initial/controller/verifier/GlobalService.java b/src/main/java/com/sktelecom/initial/controller/verifier/GlobalService.java new file mode 100644 index 0000000..1212377 --- /dev/null +++ b/src/main/java/com/sktelecom/initial/controller/verifier/GlobalService.java @@ -0,0 +1,263 @@ +package com.sktelecom.initial.controller.verifier; + +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.PathNotFoundException; +import com.sktelecom.initial.controller.utils.HttpClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import java.util.LinkedHashMap; + +import static com.sktelecom.initial.controller.utils.Common.getTypeFromBasicMessage; +import static com.sktelecom.initial.controller.utils.Common.parseInvitationUrl; + +@RequiredArgsConstructor +@Service +@Slf4j +public class GlobalService { + private final HttpClient client = new HttpClient(); + + @Value("${agentApiUrl}") + private String agentApiUrl; // agent service api url + + @Value("${accessToken}") + private String accessToken; // controller access token + + @Value("${verifTplId}") + private String verifTplId; // verification template identifier + + String orgName; + String orgImageUrl; + String publicDid; + boolean webhookUrlIsValid = false; + + @EventListener(ApplicationReadyEvent.class) + public void initializeAfterStartup() { + provisionController(); + + log.info("Controller configurations"); + log.info("------------------------------"); + log.info("- organization name: " + orgName); + log.info("- organization imageUrl: " + orgImageUrl); + log.info("- public did: " + publicDid); + log.info("- verification template id: " + verifTplId); + log.info("- controller access token: " + accessToken); + log.info("------------------------------"); + log.info("Controller is ready"); + } + + public void handleEvent(String body) { + if (!webhookUrlIsValid) + webhookUrlIsValid = true; + + String topic = JsonPath.read(body, "$.topic"); + String state = JsonPath.read(body, "$.state"); + log.info("handleEvent >>> topic:" + topic + ", state:" + state + ", body:" + body); + + switch(topic) { + case "present_proof": + // 1. holder 가 모바일 가입증명 검증 요청을 요청 + if (state.equals("proposal_received")) { + String connectionId = JsonPath.read(body, "$.connection_id"); + sendPresentationRequest(connectionId); + } + // 2. holder 가 보낸 모바일 가입증명 검증 완료 + else if (state.equals("verified")) { + log.info("- Case (topic:" + topic + ", state:" + state + ") -> getPresentationResult"); + LinkedHashMap attrs = getPresentationResult(body); + // TODO: store user information + } + else if (state.equals("abandoned")) { + log.info("- Case (topic:" + topic + ", state:" + state + ") -> Print Error Message"); + String errorMsg = JsonPath.read(body, "$.error_msg"); + log.warn(" - error_msg: " + errorMsg); + } + break; + case "problem_report": + log.warn("- Case (topic:" + topic + ") -> Print body"); + log.warn(" - body:" + body); + break; + case "connections":; + case "basicmessages": + case "issue_credential": + case "revocation_registry": + case "issuer_cred_rev": + break; + default: + log.warn("- Warning Unexpected topic:" + topic); + } + } + + void provisionController() { + log.info("TEST - Create invitation-url"); + String invitationUrl = createInvitationUrl(); + if (invitationUrl == null) { + log.info("- FAILED: Check if accessToken is valid - " + accessToken); + System.exit(0); + } + String invitation = parseInvitationUrl(invitationUrl); + publicDid = JsonPath.read(invitation, "$.did"); + orgName = JsonPath.read(invitation, "$.label"); + orgImageUrl = JsonPath.read(invitation, "$.imageUrl"); + log.info("- SUCCESS"); + + if (!verifTplId.equals("")) { + log.info("TEST - Check if verification template is valid"); + String response = client.requestGET(agentApiUrl + "/verification-templates/" + verifTplId, accessToken); + log.info("response: " + response); + LinkedHashMap verifTpl = JsonPath.read(response, "$.verification_template"); + if (verifTpl == null) { + log.info("- FAILED: " + verifTplId + " does not exists - Check if it is valid"); + System.exit(0); + } + log.info("- SUCCESS : " + verifTplId + " exists"); + } + + log.info("TEST - Check if webhook url (in console) is valid"); + // create non-public invitation to receive webhook message + client.requestPOST(agentApiUrl + "/connections/create-invitation", accessToken, "{}"); + try { + Thread.sleep(1000); // wait to receive webhook message + } catch (InterruptedException e) {} + if (!webhookUrlIsValid) { + log.info("- FAILED: webhook message is not received - Check if it is valid in console configuration"); + System.exit(0); + } + log.info("- SUCCESS"); + + } + + public String createInvitationUrl() { + String params = "?public=true"; + String response = client.requestPOST(agentApiUrl + "/connections/create-invitation" + params, accessToken, "{}"); + log.info("response: " + response); + try { + return JsonPath.read(response, "$.invitation_url"); + } catch (IllegalArgumentException e) { + return null; + } + } + + public void sendPresentationRequest(String connectionId) { + // sample agreement + String agreement = JsonPath.parse("{" + + " type: 'initial_agreement',"+ + " content: [" + + " {" + + " sequence: 1," + + " title: '개인정보 수집 및 이용 동의서'," + + " is_mandatory: 'true'," + + " terms_id: 'person'," + + " terms_ver: '1.0'," + + " agreement: 'initial서비스(이하“서비스”라 한다)와 관련하여, 본인은 동의 내용을 숙지하였으며, 이에 따라 본인의 개인정보를 귀사(SK텔레콤주식회사)가 수집 및 이용하는 것에 대해 동의 합니다. 본 동의는 서비스의 본질적 기능 제공을 위한 개인정보 수집/이용에 대한 동의로서, 동의를 하는 경우에만 서비스 이용이 가능합니다. 법령에 따른 개인정보의 수집/이용, 계약의 이행/편익 제공을 위한 개인정보 취급 위탁 및 개인정보 취급과 관련된 일반 사항은 서비스의 개인정보 처리 방침에 따릅니다.'," + + " condition: [" + + " {" + + " sub_title: '수집 항목'," + + " target: '이름,생년월일'" + + " }," + + " {" + + " sub_title: '수집 및 이용목적'," + + " target: '서비스 이용에 따른 본인확인'" + + " }," + + " {" + + " sub_title: '이용기간 및 보유/파기'," + + " target: '1년'" + + " }" + + " ]" + + " }," + + " {" + + " sequence: 2," + + " title: '위치정보 수집 및 이용 동의서'," + + " is_mandatory: 'true'," + + " terms_id: 'location'," + + " terms_ver: '1.0'," + + " agreement: '이 약관은 이니셜(SK텔레콤)(이하“회사”)가 제공하는 위치 정보사업 또는 위치기반 서비스 사업과 관련하여 회사와 개인 위치 정보주체와의 권리, 의무 및 책임사항, 기타 필요한 사항을 규정함을 목적으로 합니다.'," + + " condition: [" + + " {" + + " sub_title: '위치정보 수집 방법'," + + " target: 'GPS칩'" + + " }," + + " {" + + " sub_title: '위치정보 이용/제공'," + + " target: '이 약관에 명시되지 않은 사항은 위치정보의 보호 및 이용 등에 관한 법률, 정보통신망 이용촉진 및 정보보호 등에 관한 법률, 전기통신기본법, 전기통신사업법 등 관계법령과 회사의 이용약관 및 개인정보취급방침, 회사가 별도로 정한 지침 등에 의합니다.'" + + " }," + + " {" + + " sub_title: '수집목적'," + + " target: '현재의 위치를 기반으로 하여 주변 매장의 위치 등의 정보를 제공하는 서비스'" + + " }," + + " {" + + " sub_title: '위치정보 보유기간'," + + " target: '1년'" + + " }" + + " ]" + + " }," + + " {" + + " sequence: 3," + + " title: '제3자 정보제공 동의서'," + + " is_mandatory: 'true'," + + " terms_id: '3rdparty'," + + " terms_ver: '1.0'," + + " agreement: 'initial서비스(이하“서비스”라 한다)와 관련하여, 본인은 동의 내용을 숙지하였으며, 이에 따라 본인의 개인정보를 귀사(이슈어)가 수집한 개인정보를 아래와 같이 제3자에게 제공하는 것에 대해 동의 합니다. 고객은 개인정보의 제3자 제공에 대한 동의를 거부할 권리가 있으며, 동의를 거부할 시 받는 별도의 불이익은 없습니다. 단, 서비스 이용이 불가능하거나, 서비스 이용 목적에 따른 서비스 제공에 제한이 따르게 됩니다.'," + + " condition: [" + + " {" + + " sub_title: '제공하는 자'," + + " target: '발급기관'" + + " }," + + " {" + + " sub_title: '제공받는 자'," + + " target: '이니셜(SK텔레콤)'" + + " }," + + " {" + + " sub_title: '제공받는 항목'," + + " target: '생년월일,시험일,성명(영문),만료일,성명(한글),수험번호,듣기점수,읽기점수,총점'" + + " }," + + " {" + + " sub_title: '수집 및 이용목적'," + + " target: '모바일 전자증명서 발급'" + + " }," + + " {" + + " sub_title: '보유 및 이용기간'," + + " target: '모바일 전자증명서 발급을 위해 서버에 임시 저장하였다가, 증명서 발행 후 즉시 삭제(단, 고객 단말기 내부 저장영역에 증명서 형태로 저장/보관)'" + + " }" + + " ]" + + " }" + + " ]"+ + "}").jsonString(); + + String body = JsonPath.parse("{" + + " connection_id: '" + connectionId + "'," + + " verification_template_id: '" + verifTplId + "'," + + " agreement: " + agreement + + "}").jsonString(); + String response = client.requestPOST(agentApiUrl + "/present-proof/send-verification-request", accessToken, body); + log.info("response: " + response); + } + + public LinkedHashMap getPresentationResult(String presExRecord) { + String verified = JsonPath.read(presExRecord, "$.verified"); + if (!verified.equals("true")) { + log.info("proof is not verified"); + log.info("Possible Reason: Revoked or Signature mismatch or Predicates unsatisfied"); + return null; + } + String requestedProof = JsonPath.parse((LinkedHashMap)JsonPath.read(presExRecord, "$.presentation.requested_proof")).jsonString(); + + LinkedHashMap revealedAttrs = JsonPath.read(requestedProof, "$.revealed_attrs"); + LinkedHashMap attrs = new LinkedHashMap<>(); + for(String key : revealedAttrs.keySet()) + attrs.put(key, JsonPath.read(revealedAttrs.get(key), "$.raw")); + for(String key : attrs.keySet()) + log.info("Requested Attribute - " + key + ": " + attrs.get(key)); + + LinkedHashMap predicates = JsonPath.read(requestedProof, "$.predicates"); + for(String key : predicates.keySet()) + log.info("Requested Predicates - " + key + " is satisfied"); + + return attrs; + } + +} \ No newline at end of file diff --git a/src/main/resources/application-holder-prod.properties b/src/main/resources/application-holder-prod.properties deleted file mode 100644 index c96c2ae..0000000 --- a/src/main/resources/application-holder-prod.properties +++ /dev/null @@ -1,5 +0,0 @@ -server.port = 8041 -agentApiUrl = https://console.myinitial.io/agent/api -accessToken = 3a0ece13-dd04-419d-b3ea-f12b52e297d7 -issuerInvitationUrl = https://issuer-controller.url/invitation-url -issuerCredDefId = Qr7Yo4sPs7cXiiVbEYwGsJ:3:CL:1618984624:1ee53b6d-7d8c-461e-910a-623302dc854a diff --git a/src/main/resources/application-holder.properties b/src/main/resources/application-holder.properties deleted file mode 100644 index 864b2a1..0000000 --- a/src/main/resources/application-holder.properties +++ /dev/null @@ -1,5 +0,0 @@ -server.port = 8041 -agentApiUrl = https://dev-console.myinitial.io/agent/api -accessToken = 3a0ece13-dd04-419d-b3ea-f12b52e297d7 -issuerInvitationUrl = https://issuer-controller.url/invitation-url -issuerCredDefId = Qr7Yo4sPs7cXiiVbEYwGsJ:3:CL:1618984624:1ee53b6d-7d8c-461e-910a-623302dc854a diff --git a/src/main/resources/application-holder_webhook.properties b/src/main/resources/application-holder_webhook.properties new file mode 100644 index 0000000..22ac6bc --- /dev/null +++ b/src/main/resources/application-holder_webhook.properties @@ -0,0 +1,17 @@ +server.port = 8041 +agentApiUrl = https://dev-console.myinitial.io/agent/api +accessToken = 514ac4f8-e0da-43c9-910d-4894279909b2 + +mobileIssuerInvitationUrl = https://dev-console.myinitial.io/mobile-issuer-v2/invitation-url +mobileCredDefId = TBz5HEP6gzwqDDMw3Ci7BU:3:CL:1618987943:4224f310-cd2b-4836-843b-07b666c2bf6b +mobileCredProposalEncodeData = 9INN4y9uwW5M6lZ6GvvaLUjfPoq25xDuD2XZVF7GlpIbKLwf7SJJgRoIhnx3hXwYfc99b5HQ0QFSMQb4BoDXjnV3/WgeAXjgeP4WACN0OSyneD8DhB9nRmda1zkQVQ2WseKtjLD4FZRsbtZWVu5m5fJ1+P76bmYd7nFA3p1HX6oUocY0QVbGhsYRr/nCNccr/5+zOc2ghzVpD2P3KREOx6rZD1DDWWR2PMl6GOth3QXqF9aXSBdbXWKi9imG/QfsVDQwNxbqY6i2yRfkjGYM1P+eIKknUoNJueMzDgEib3K7sV7YZUm+KH2K7ePvAs09 +mobileCredProposalSite = mobile-wallet +mobileCredProposalReqSeq = REQ_SEQ_VALUE + +tpIssuerInvitationUrl = http://221.168.33.105:8043/invitation-url +tpCredDefId = SxK6LzvMTgmCPTBm3LwRvG:3:CL:1618984624:43769670-fa4d-4aa8-94c6-e281a46726f0 + +tpVerifierInvitationUrl = http://221.168.33.105:8041/invitation-url + +# issuer or verifier +runType = issuer \ No newline at end of file diff --git a/src/main/resources/application-issuer-prod.properties b/src/main/resources/application-issuer-prod.properties index ff9119f..a7b41b3 100644 --- a/src/main/resources/application-issuer-prod.properties +++ b/src/main/resources/application-issuer-prod.properties @@ -4,3 +4,6 @@ accessToken = 514ac4f8-e0da-43c9-910d-4894279909b2 credDefId = Qr7Yo4sPs7cXiiVbEYwGsJ:3:CL:1618984624:1ee53b6d-7d8c-461e-910a-623302dc854a verifTplId = 0012d683-bdb0-4050-ac85-ae37e59bad09 webViewUrl = https://issuer-controller.url/web-view/form.html +isEncrypted = false +cipherKey = 7254f296e57d4ac589ca4125037835e4 +cipherIvKey = 72fe5dc578d641388e91d9409317f802 diff --git a/src/main/resources/application-issuer.properties b/src/main/resources/application-issuer.properties index a02f3e3..0f21bee 100644 --- a/src/main/resources/application-issuer.properties +++ b/src/main/resources/application-issuer.properties @@ -3,4 +3,7 @@ agentApiUrl = https://dev-console.myinitial.io/agent/api accessToken = 514ac4f8-e0da-43c9-910d-4894279909b2 credDefId = Qr7Yo4sPs7cXiiVbEYwGsJ:3:CL:1618984624:1ee53b6d-7d8c-461e-910a-623302dc854a verifTplId = 0012d683-bdb0-4050-ac85-ae37e59bad09 -webViewUrl = https://issuer-controller.url/web-view/form.html \ No newline at end of file +webViewUrl = https://issuer-controller.url/web-view/form.html +isEncrypted = false +cipherKey = 7254f296e57d4ac589ca4125037835e4 +cipherIvKey = 72fe5dc578d641388e91d9409317f802 diff --git a/src/main/resources/application-verifier-prod.properties b/src/main/resources/application-verifier-prod.properties new file mode 100644 index 0000000..f3510a2 --- /dev/null +++ b/src/main/resources/application-verifier-prod.properties @@ -0,0 +1,7 @@ +server.port = 8040 +agentApiUrl = https://console.myinitial.io/agent/api +accessToken = 514ac4f8-e0da-43c9-910d-4894279909b2 +verifTplId = 0012d683-bdb0-4050-ac85-ae37e59bad09 +isEncrypted = false +cipherKey = 7254f296e57d4ac589ca4125037835e4 +cipherIvKey = 72fe5dc578d641388e91d9409317f802 diff --git a/src/main/resources/application-verifier.properties b/src/main/resources/application-verifier.properties new file mode 100644 index 0000000..147029a --- /dev/null +++ b/src/main/resources/application-verifier.properties @@ -0,0 +1,7 @@ +server.port = 8040 +agentApiUrl = https://dev-console.myinitial.io/agent/api +accessToken = 514ac4f8-e0da-43c9-910d-4894279909b2 +verifTplId = 0012d683-bdb0-4050-ac85-ae37e59bad09 +isEncrypted = false +cipherKey = 7254f296e57d4ac589ca4125037835e4 +cipherIvKey = 72fe5dc578d641388e91d9409317f802 diff --git a/src/main/resources/static/web-view/form.html b/src/main/resources/static/web-view/form.html index b0446a1..b412832 100644 --- a/src/main/resources/static/web-view/form.html +++ b/src/main/resources/static/web-view/form.html @@ -45,6 +45,7 @@ + web-view form