1340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang/*
2340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * Copyright (C) 2013 The Android Open Source Project
3340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang *
4340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * Licensed under the Apache License, Version 2.0 (the "License");
5340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * you may not use this file except in compliance with the License.
6340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * You may obtain a copy of the License at
7340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang *
8340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang *      http://www.apache.org/licenses/LICENSE-2.0
9340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang *
10340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * Unless required by applicable law or agreed to in writing, software
11340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * distributed under the License is distributed on an "AS IS" BASIS,
12340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * See the License for the specific language governing permissions and
14340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * limitations under the License.
15340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang */
16340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
17340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangpackage com.android.settings.vpn2;
18340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
19340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport android.os.Environment;
20340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport android.security.Credentials;
21340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport android.security.KeyStore;
22340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport android.util.Log;
23340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
24340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport com.android.internal.net.VpnProfile;
25340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport com.android.org.bouncycastle.asn1.ASN1InputStream;
26340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport com.android.org.bouncycastle.asn1.ASN1Sequence;
27340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport com.android.org.bouncycastle.asn1.DEROctetString;
28340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport com.android.org.bouncycastle.asn1.x509.BasicConstraints;
29340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
30340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport junit.framework.Assert;
31340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
32340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport libcore.io.Streams;
33340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
34340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.io.ByteArrayInputStream;
35340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.io.File;
36340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.io.FileInputStream;
37340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.io.IOException;
38340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.io.InputStream;
39340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.nio.charset.StandardCharsets;
40340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.security.KeyStoreException;
41340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.security.NoSuchAlgorithmException;
42340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.security.KeyStore.PasswordProtection;
43340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.security.KeyStore.PrivateKeyEntry;
44340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.security.PrivateKey;
45340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.security.UnrecoverableEntryException;
46340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.security.cert.Certificate;
47340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.security.cert.CertificateEncodingException;
48340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.security.cert.CertificateException;
49340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.security.cert.X509Certificate;
50340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.util.ArrayList;
51340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.util.Collections;
52340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.util.Enumeration;
53340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangimport java.util.List;
54340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
55340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang/**
56340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * Certificate installer helper to extract information from a provided file
57340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang * and install certificates to keystore.
58340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang */
59340bda7154194d64a719fb5c86a702a4e5773be0Xia Wangpublic class CertInstallerHelper {
60340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    private static final String TAG = "CertInstallerHelper";
61340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    /* Define a password to unlock keystore after it is reset */
62340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    private static final String CERT_STORE_PASSWORD = "password";
63340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    private final int mUid = KeyStore.UID_SELF;
64340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    private PrivateKey mUserKey;  // private key
65340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    private X509Certificate mUserCert;  // user certificate
66340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    private List<X509Certificate> mCaCerts = new ArrayList<X509Certificate>();
67340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    private KeyStore mKeyStore = KeyStore.getInstance();
68340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
69340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    /**
70340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang     * Unlock keystore and set password
71340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang     */
72340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    public CertInstallerHelper() {
73340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        mKeyStore.reset();
747236f2abbabefc08e68226677f51fe2117c8c0a5Chad Brubaker        mKeyStore.onUserPasswordChanged(CERT_STORE_PASSWORD);
75340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    }
76340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
77340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    private void extractCertificate(String certFile, String password) {
78340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        InputStream in = null;
79340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        final byte[] raw;
80340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        java.security.KeyStore keystore = null;
81340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        try {
82340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            // Read .p12 file from SDCARD and extract with password
83340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            in = new FileInputStream(new File(
84340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    Environment.getExternalStorageDirectory(), certFile));
85340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            raw = Streams.readFully(in);
86340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
87340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            keystore = java.security.KeyStore.getInstance("PKCS12");
88340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            PasswordProtection passwordProtection = new PasswordProtection(password.toCharArray());
89340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            keystore.load(new ByteArrayInputStream(raw), passwordProtection.getPassword());
90340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
91340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            // Install certificates and private keys
92340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            Enumeration<String> aliases = keystore.aliases();
93340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            if (!aliases.hasMoreElements()) {
94340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                Assert.fail("key store failed to put in keychain");
95340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            }
96340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            ArrayList<String> aliasesList = Collections.list(aliases);
97340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            // The keystore is initialized for each test case, there will
98340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            // be only one alias in the keystore
99340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            Assert.assertEquals(1, aliasesList.size());
100340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            String alias = aliasesList.get(0);
101340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            java.security.KeyStore.Entry entry = keystore.getEntry(alias, passwordProtection);
102340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            Log.d(TAG, "extracted alias = " + alias + ", entry=" + entry.getClass());
103340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
104340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            if (entry instanceof PrivateKeyEntry) {
105340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                Assert.assertTrue(installFrom((PrivateKeyEntry) entry));
106340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            }
107340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        } catch (IOException e) {
108340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            Assert.fail("Failed to read certficate: " + e);
109340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        } catch (KeyStoreException e) {
110340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            Log.e(TAG, "failed to extract certificate" + e);
111340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        } catch (NoSuchAlgorithmException e) {
112340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            Log.e(TAG, "failed to extract certificate" + e);
113340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        } catch (CertificateException e) {
114340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            Log.e(TAG, "failed to extract certificate" + e);
115340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        } catch (UnrecoverableEntryException e) {
116340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            Log.e(TAG, "failed to extract certificate" + e);
117340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        }
118340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        finally {
119340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            if (in != null) {
120340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                try {
121340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    in.close();
122340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                } catch (IOException e) {
123340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    Log.e(TAG, "close FileInputStream error: " + e);
124340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                }
125340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            }
126340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        }
127340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    }
128340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
129340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    /**
130340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang     * Extract private keys, user certificates and ca certificates
131340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang     */
132340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    private synchronized boolean installFrom(PrivateKeyEntry entry) {
133340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        mUserKey = entry.getPrivateKey();
134340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        mUserCert = (X509Certificate) entry.getCertificate();
135340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
136340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        Certificate[] certs = entry.getCertificateChain();
137340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        Log.d(TAG, "# certs extracted = " + certs.length);
138340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        mCaCerts = new ArrayList<X509Certificate>(certs.length);
139340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        for (Certificate c : certs) {
140340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            X509Certificate cert = (X509Certificate) c;
141340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            if (isCa(cert)) {
142340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                mCaCerts.add(cert);
143340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            }
144340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        }
145340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        Log.d(TAG, "# ca certs extracted = " + mCaCerts.size());
146340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        return true;
147340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    }
148340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
149340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    private boolean isCa(X509Certificate cert) {
150340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        try {
151340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            byte[] asn1EncodedBytes = cert.getExtensionValue("2.5.29.19");
152340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            if (asn1EncodedBytes == null) {
153340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                return false;
154340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            }
155340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            DEROctetString derOctetString = (DEROctetString)
156340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    new ASN1InputStream(asn1EncodedBytes).readObject();
157340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            byte[] octets = derOctetString.getOctets();
158340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            ASN1Sequence sequence = (ASN1Sequence)
159340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    new ASN1InputStream(octets).readObject();
160340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            return BasicConstraints.getInstance(sequence).isCA();
161340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        } catch (IOException e) {
162340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            return false;
163340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        }
164340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    }
165340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
166340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    /**
167340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang     * Extract certificate from the given file, and install it to keystore
168340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang     * @param name certificate name
169340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang     * @param certFile .p12 file which includes certificates
170340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang     * @param password password to extract the .p12 file
171340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang     */
172340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    public void installCertificate(VpnProfile profile, String certFile, String password) {
173340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        // extract private keys, certificates from the provided file
174340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        extractCertificate(certFile, password);
175340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        // install certificate to the keystore
176340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        int flags = KeyStore.FLAG_ENCRYPTED;
177340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        try {
178340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            if (mUserKey != null) {
179340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                Log.v(TAG, "has private key");
180340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                String key = Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert;
181340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                byte[] value = mUserKey.getEncoded();
182340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
183340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                if (!mKeyStore.importKey(key, value, mUid, flags)) {
184340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    Log.e(TAG, "Failed to install " + key + " as user " + mUid);
185340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    return;
186340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                }
187340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                Log.v(TAG, "install " + key + " as user " + mUid + " is successful");
188340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            }
189340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
190340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            if (mUserCert != null) {
191340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                String certName = Credentials.USER_CERTIFICATE + profile.ipsecUserCert;
192340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                byte[] certData = Credentials.convertToPem(mUserCert);
193340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
194340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                if (!mKeyStore.put(certName, certData, mUid, flags)) {
195340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    Log.e(TAG, "Failed to install " + certName + " as user " + mUid);
196340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    return;
197340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                }
198340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                Log.v(TAG, "install " + certName + " as user" + mUid + " is successful.");
199340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            }
200340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
201340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            if (!mCaCerts.isEmpty()) {
202340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                String caListName = Credentials.CA_CERTIFICATE + profile.ipsecCaCert;
203340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                X509Certificate[] caCerts = mCaCerts.toArray(new X509Certificate[mCaCerts.size()]);
204340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                byte[] caListData = Credentials.convertToPem(caCerts);
205340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
206340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                if (!mKeyStore.put(caListName, caListData, mUid, flags)) {
207340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    Log.e(TAG, "Failed to install " + caListName + " as user " + mUid);
208340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                    return;
209340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                }
210340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang                Log.v(TAG, " install " + caListName + " as user " + mUid + " is successful");
211340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            }
212340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        } catch (CertificateEncodingException e) {
213340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            Log.e(TAG, "Exception while convert certificates to pem " + e);
214340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            throw new AssertionError(e);
215340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        } catch (IOException e) {
216340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang            Log.e(TAG, "IOException while convert to pem: " + e);
217340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        }
218340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    }
219340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang
220340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    public int getUid() {
221340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang        return mUid;
222340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang    }
223340bda7154194d64a719fb5c86a702a4e5773be0Xia Wang}
224