1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.android_webview; 6 7 import android.net.http.SslCertificate; 8 import android.net.http.SslError; 9 import android.util.Log; 10 import android.webkit.ValueCallback; 11 12 import org.chromium.base.CalledByNative; 13 import org.chromium.base.JNINamespace; 14 import org.chromium.base.ThreadUtils; 15 import org.chromium.net.AndroidPrivateKey; 16 import org.chromium.net.DefaultAndroidKeyStore; 17 18 import java.security.Principal; 19 import java.security.PrivateKey; 20 import java.security.cert.CertificateEncodingException; 21 import java.security.cert.X509Certificate; 22 23 import javax.security.auth.x500.X500Principal; 24 25 /** 26 * This class handles the JNI communication logic for the the AwContentsClient class. 27 * Both the Java and the native peers of AwContentsClientBridge are owned by the 28 * corresponding AwContents instances. This class and its native peer are connected 29 * via weak references. The native AwContentsClientBridge sets up and clear these weak 30 * references. 31 */ 32 @JNINamespace("android_webview") 33 public class AwContentsClientBridge { 34 static final String TAG = "AwContentsClientBridge"; 35 36 private AwContentsClient mClient; 37 // The native peer of this object. 38 private long mNativeContentsClientBridge; 39 40 private DefaultAndroidKeyStore mLocalKeyStore; 41 42 private ClientCertLookupTable mLookupTable; 43 44 // Used for mocking this class in tests. 45 protected AwContentsClientBridge(DefaultAndroidKeyStore keyStore, 46 ClientCertLookupTable table) { 47 mLocalKeyStore = keyStore; 48 mLookupTable = table; 49 } 50 51 public AwContentsClientBridge(AwContentsClient client, DefaultAndroidKeyStore keyStore, 52 ClientCertLookupTable table) { 53 assert client != null; 54 mClient = client; 55 mLocalKeyStore = keyStore; 56 mLookupTable = table; 57 } 58 59 /** 60 * Callback to communicate clientcertificaterequest back to the AwContentsClientBridge. 61 * The public methods should be called on UI thread. 62 * A request can not be proceeded, ignored or canceled more than once. Doing this 63 * is a programming error and causes an exception. 64 */ 65 public class ClientCertificateRequestCallback { 66 67 private int mId; 68 private String mHost; 69 private int mPort; 70 private boolean mIsCalled; 71 72 public ClientCertificateRequestCallback(int id, String host, int port) { 73 mId = id; 74 mHost = host; 75 mPort = port; 76 } 77 78 public void proceed(final PrivateKey privateKey, final X509Certificate[] chain) { 79 ThreadUtils.runOnUiThread(new Runnable() { 80 @Override 81 public void run() { 82 proceedOnUiThread(privateKey, chain); 83 } 84 }); 85 } 86 87 public void ignore() { 88 ThreadUtils.runOnUiThread(new Runnable() { 89 @Override 90 public void run() { 91 ignoreOnUiThread(); 92 } 93 }); 94 } 95 96 public void cancel() { 97 ThreadUtils.runOnUiThread(new Runnable() { 98 @Override 99 public void run() { 100 cancelOnUiThread(); 101 } 102 103 }); 104 } 105 106 private void proceedOnUiThread(PrivateKey privateKey, X509Certificate[] chain) { 107 checkIfCalled(); 108 109 AndroidPrivateKey key = mLocalKeyStore.createKey(privateKey); 110 111 if (key == null || chain == null || chain.length == 0) { 112 Log.w(TAG, "Empty client certificate chain?"); 113 provideResponse(null, null); 114 return; 115 } 116 // Encode the certificate chain. 117 byte[][] encodedChain = new byte[chain.length][]; 118 try { 119 for (int i = 0; i < chain.length; ++i) { 120 encodedChain[i] = chain[i].getEncoded(); 121 } 122 } catch (CertificateEncodingException e) { 123 Log.w(TAG, "Could not retrieve encoded certificate chain: " + e); 124 provideResponse(null, null); 125 return; 126 } 127 mLookupTable.allow(mHost, mPort, key, encodedChain); 128 provideResponse(key, encodedChain); 129 } 130 131 private void ignoreOnUiThread() { 132 checkIfCalled(); 133 provideResponse(null, null); 134 } 135 136 private void cancelOnUiThread() { 137 checkIfCalled(); 138 mLookupTable.deny(mHost, mPort); 139 provideResponse(null, null); 140 } 141 142 private void checkIfCalled() { 143 if (mIsCalled) { 144 throw new IllegalStateException("The callback was already called."); 145 } 146 mIsCalled = true; 147 } 148 149 private void provideResponse(AndroidPrivateKey androidKey, byte[][] certChain) { 150 if (mNativeContentsClientBridge == 0) return; 151 nativeProvideClientCertificateResponse(mNativeContentsClientBridge, mId, 152 certChain, androidKey); 153 } 154 } 155 156 // Used by the native peer to set/reset a weak ref to the native peer. 157 @CalledByNative 158 private void setNativeContentsClientBridge(long nativeContentsClientBridge) { 159 mNativeContentsClientBridge = nativeContentsClientBridge; 160 } 161 162 // If returns false, the request is immediately canceled, and any call to proceedSslError 163 // has no effect. If returns true, the request should be canceled or proceeded using 164 // proceedSslError(). 165 // Unlike the webview classic, we do not keep keep a database of certificates that 166 // are allowed by the user, because this functionality is already handled via 167 // ssl_policy in native layers. 168 @CalledByNative 169 private boolean allowCertificateError(int certError, byte[] derBytes, final String url, 170 final int id) { 171 final SslCertificate cert = SslUtil.getCertificateFromDerBytes(derBytes); 172 if (cert == null) { 173 // if the certificate or the client is null, cancel the request 174 return false; 175 } 176 final SslError sslError = SslUtil.sslErrorFromNetErrorCode(certError, cert, url); 177 ValueCallback<Boolean> callback = new ValueCallback<Boolean>() { 178 @Override 179 public void onReceiveValue(final Boolean value) { 180 ThreadUtils.runOnUiThread(new Runnable() { 181 @Override 182 public void run() { 183 proceedSslError(value.booleanValue(), id); 184 } 185 }); 186 } 187 }; 188 mClient.onReceivedSslError(callback, sslError); 189 return true; 190 } 191 192 private void proceedSslError(boolean proceed, int id) { 193 if (mNativeContentsClientBridge == 0) return; 194 nativeProceedSslError(mNativeContentsClientBridge, proceed, id); 195 } 196 197 // Intentionally not private for testing the native peer of this class. 198 @CalledByNative 199 protected void selectClientCertificate(final int id, final String[] keyTypes, 200 byte[][] encodedPrincipals, final String host, final int port) { 201 assert mNativeContentsClientBridge != 0; 202 ClientCertLookupTable.Cert cert = mLookupTable.getCertData(host, port); 203 if (mLookupTable.isDenied(host, port)) { 204 nativeProvideClientCertificateResponse(mNativeContentsClientBridge, id, 205 null, null); 206 return; 207 } 208 if (cert != null) { 209 nativeProvideClientCertificateResponse(mNativeContentsClientBridge, id, 210 cert.mCertChain, cert.mPrivateKey); 211 return; 212 } 213 // Build the list of principals from encoded versions. 214 Principal[] principals = null; 215 if (encodedPrincipals.length > 0) { 216 principals = new X500Principal[encodedPrincipals.length]; 217 for (int n = 0; n < encodedPrincipals.length; n++) { 218 try { 219 principals[n] = new X500Principal(encodedPrincipals[n]); 220 } catch (IllegalArgumentException e) { 221 Log.w(TAG, "Exception while decoding issuers list: " + e); 222 nativeProvideClientCertificateResponse(mNativeContentsClientBridge, id, 223 null, null); 224 return; 225 } 226 } 227 228 } 229 230 final ClientCertificateRequestCallback callback = 231 new ClientCertificateRequestCallback(id, host, port); 232 mClient.onReceivedClientCertRequest(callback, keyTypes, principals, host, port); 233 } 234 235 @CalledByNative 236 private void handleJsAlert(String url, String message, int id) { 237 JsResultHandler handler = new JsResultHandler(this, id); 238 mClient.handleJsAlert(url, message, handler); 239 } 240 241 @CalledByNative 242 private void handleJsConfirm(String url, String message, int id) { 243 JsResultHandler handler = new JsResultHandler(this, id); 244 mClient.handleJsConfirm(url, message, handler); 245 } 246 247 @CalledByNative 248 private void handleJsPrompt(String url, String message, String defaultValue, int id) { 249 JsResultHandler handler = new JsResultHandler(this, id); 250 mClient.handleJsPrompt(url, message, defaultValue, handler); 251 } 252 253 @CalledByNative 254 private void handleJsBeforeUnload(String url, String message, int id) { 255 JsResultHandler handler = new JsResultHandler(this, id); 256 mClient.handleJsBeforeUnload(url, message, handler); 257 } 258 259 @CalledByNative 260 private boolean shouldOverrideUrlLoading(String url) { 261 return mClient.shouldOverrideUrlLoading(url); 262 } 263 264 void confirmJsResult(int id, String prompt) { 265 if (mNativeContentsClientBridge == 0) return; 266 nativeConfirmJsResult(mNativeContentsClientBridge, id, prompt); 267 } 268 269 void cancelJsResult(int id) { 270 if (mNativeContentsClientBridge == 0) return; 271 nativeCancelJsResult(mNativeContentsClientBridge, id); 272 } 273 274 //-------------------------------------------------------------------------------------------- 275 // Native methods 276 //-------------------------------------------------------------------------------------------- 277 private native void nativeProceedSslError(long nativeAwContentsClientBridge, boolean proceed, 278 int id); 279 private native void nativeProvideClientCertificateResponse(long nativeAwContentsClientBridge, 280 int id, byte[][] certChain, AndroidPrivateKey androidKey); 281 282 private native void nativeConfirmJsResult(long nativeAwContentsClientBridge, int id, 283 String prompt); 284 private native void nativeCancelJsResult(long nativeAwContentsClientBridge, int id); 285 } 286