16a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen/* 26a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Copyright (C) 2015 The Android Open Source Project 36a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 46a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Licensed under the Apache License, Version 2.0 (the "License"); 56a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * you may not use this file except in compliance with the License. 66a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * You may obtain a copy of the License at 76a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 86a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * http://www.apache.org/licenses/LICENSE-2.0 96a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 106a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Unless required by applicable law or agreed to in writing, software 116a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * distributed under the License is distributed on an "AS IS" BASIS, 126a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 136a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * See the License for the specific language governing permissions and 146a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * limitations under the License. 156a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 166a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 176a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenpackage com.android.statementservice.retriever; 186a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 196a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport org.json.JSONArray; 206a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport org.json.JSONException; 216a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport org.json.JSONObject; 226a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 236a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.util.ArrayList; 246a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.util.Collections; 256a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.util.HashSet; 266a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.util.List; 276a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wenimport java.util.Locale; 286a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 296a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen/** 306a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Immutable value type that names an Android app asset. 316a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 326a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * <p>An Android app can be named by its package name and certificate fingerprints using this JSON 336a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * string: { "namespace": "android_app", "package_name": "[Java package name]", 346a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * "sha256_cert_fingerprints": ["[SHA256 fingerprint of signing cert]", "[additional cert]", ...] } 356a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 366a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * <p>For example, { "namespace": "android_app", "package_name": "com.test.mytestapp", 376a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * "sha256_cert_fingerprints": ["24:D9:B4:57:A6:42:FB:E6:E5:B8:D6:9E:7B:2D:C2:D1:CB:D1:77:17:1D:7F:D4:A9:16:10:11:AB:92:B9:8F:3F"] 386a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * } 396a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 406a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using: 416a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * {@code keytool -list -printcert -jarfile signed_app.apk} 426a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 436a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * <p>Each entry in "sha256_cert_fingerprints" is a colon-separated hex string (e.g. 14:6D:E9:...) 446a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * representing the certificate SHA-256 fingerprint. 456a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 466a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen/* package private */ final class AndroidAppAsset extends AbstractAsset { 476a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 486a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set."; 496a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private static final String MISSING_APPCERTS_FORMAT_STRING = 506a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen "Expected %s to be non-empty array."; 516a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private static final String APPCERT_NOT_STRING_FORMAT_STRING = "Expected all %s to be strings."; 526a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 536a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private final List<String> mCertFingerprints; 546a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private final String mPackageName; 556a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 566a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public List<String> getCertFingerprints() { 576a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return Collections.unmodifiableList(mCertFingerprints); 586a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 596a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 606a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public String getPackageName() { 616a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return mPackageName; 626a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 636a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 646a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen @Override 656a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public String toJson() { 666a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen AssetJsonWriter writer = new AssetJsonWriter(); 676a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 686a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_ANDROID_APP); 696a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen writer.writeFieldLower(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName); 706a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen writer.writeArrayUpper(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints); 716a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 726a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return writer.closeAndGetString(); 736a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 746a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 756a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen @Override 766a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public String toString() { 776a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen StringBuilder asset = new StringBuilder(); 786a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen asset.append("AndroidAppAsset: "); 796a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen asset.append(toJson()); 806a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return asset.toString(); 816a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 826a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 836a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen @Override 846a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public boolean equals(Object o) { 856a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (!(o instanceof AndroidAppAsset)) { 866a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return false; 876a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 886a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 896a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return ((AndroidAppAsset) o).toJson().equals(toJson()); 906a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 916a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 926a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen @Override 936a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public int hashCode() { 946a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return toJson().hashCode(); 956a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 966a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 976a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen @Override 986a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public int lookupKey() { 996a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return getPackageName().hashCode(); 1006a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1016a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 102aef66921e9bf8147cb0f1444ebdb102016d69f3bJoseph Wen @Override 103aef66921e9bf8147cb0f1444ebdb102016d69f3bJoseph Wen public boolean followInsecureInclude() { 104aef66921e9bf8147cb0f1444ebdb102016d69f3bJoseph Wen // Non-HTTPS includes are not allowed in Android App assets. 105aef66921e9bf8147cb0f1444ebdb102016d69f3bJoseph Wen return false; 106aef66921e9bf8147cb0f1444ebdb102016d69f3bJoseph Wen } 107aef66921e9bf8147cb0f1444ebdb102016d69f3bJoseph Wen 1086a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen /** 1096a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Checks that the input is a valid Android app asset. 1106a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 1116a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * @param asset a JSONObject that has "namespace", "package_name", and 1126a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * "sha256_cert_fingerprints" fields. 1136a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * @throws AssociationServiceException if the asset is not well formatted. 1146a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 1156a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public static AndroidAppAsset create(JSONObject asset) 1166a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen throws AssociationServiceException { 1176a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen String packageName = asset.optString(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME); 1186a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (packageName.equals("")) { 1196a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING, 1206a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME)); 1216a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1226a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1236a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen JSONArray certArray = asset.optJSONArray(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS); 1246a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (certArray == null || certArray.length() == 0) { 1256a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen throw new AssociationServiceException( 1266a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen String.format(MISSING_APPCERTS_FORMAT_STRING, 1276a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS)); 1286a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1296a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen List<String> certFingerprints = new ArrayList<>(certArray.length()); 1306a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen for (int i = 0; i < certArray.length(); i++) { 1316a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen try { 1326a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen certFingerprints.add(certArray.getString(i)); 1336a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } catch (JSONException e) { 1346a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen throw new AssociationServiceException( 1356a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen String.format(APPCERT_NOT_STRING_FORMAT_STRING, 1366a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS)); 1376a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1386a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1396a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1406a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return new AndroidAppAsset(packageName, certFingerprints); 1416a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1426a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1436a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen /** 1446a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Creates a new AndroidAppAsset. 1456a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * 1466a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * @param packageName the package name of the Android app. 1476a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * @param certFingerprints at least one of the Android app signing certificate sha-256 1486a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * fingerprint. 1496a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 1506a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen public static AndroidAppAsset create(String packageName, List<String> certFingerprints) { 1516a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (packageName == null || packageName.equals("")) { 1526a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen throw new AssertionError("Expected packageName to be set."); 1536a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1546a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (certFingerprints == null || certFingerprints.size() == 0) { 1556a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen throw new AssertionError("Expected certFingerprints to be set."); 1566a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1576a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen List<String> lowerFps = new ArrayList<String>(certFingerprints.size()); 1586a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen for (String fp : certFingerprints) { 1596a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen lowerFps.add(fp.toUpperCase(Locale.US)); 1606a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1616a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return new AndroidAppAsset(packageName, lowerFps); 1626a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1636a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1646a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private AndroidAppAsset(String packageName, List<String> certFingerprints) { 1656a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (packageName.equals("")) { 1666a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen mPackageName = null; 1676a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } else { 1686a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen mPackageName = packageName; 1696a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1706a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1716a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (certFingerprints == null || certFingerprints.size() == 0) { 1726a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen mCertFingerprints = null; 1736a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } else { 1746a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen mCertFingerprints = Collections.unmodifiableList(sortAndDeDuplicate(certFingerprints)); 1756a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1766a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1776a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1786a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen /** 1796a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen * Returns an ASCII-sorted copy of the list of certs with all duplicates removed. 1806a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen */ 1816a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen private List<String> sortAndDeDuplicate(List<String> certs) { 1826a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen if (certs.size() <= 1) { 1836a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return certs; 1846a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1856a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen HashSet<String> set = new HashSet<>(certs); 1866a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen List<String> result = new ArrayList<>(set); 1876a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen Collections.sort(result); 1886a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen return result; 1896a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen } 1906a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen 1916a34bb2d6a6cbc7a70bdf0c53d238dc28e0b1d58Joseph Wen} 192