1 /* 2 * Copyright (C) 2009 Google Inc. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.polo.pairing; 18 19 import com.google.polo.encoding.HexadecimalEncoder; 20 import com.google.polo.encoding.SecretEncoder; 21 import com.google.polo.exception.BadSecretException; 22 import com.google.polo.exception.NoConfigurationException; 23 import com.google.polo.exception.PoloException; 24 import com.google.polo.exception.ProtocolErrorException; 25 import com.google.polo.pairing.PairingListener.LogLevel; 26 import com.google.polo.pairing.message.ConfigurationMessage; 27 import com.google.polo.pairing.message.EncodingOption; 28 import com.google.polo.pairing.message.OptionsMessage; 29 import com.google.polo.pairing.message.OptionsMessage.ProtocolRole; 30 import com.google.polo.pairing.message.PoloMessage; 31 import com.google.polo.pairing.message.PoloMessage.PoloMessageType; 32 import com.google.polo.pairing.message.SecretAckMessage; 33 import com.google.polo.pairing.message.SecretMessage; 34 import com.google.polo.wire.PoloWireInterface; 35 36 import java.io.IOException; 37 import java.security.NoSuchAlgorithmException; 38 import java.security.SecureRandom; 39 import java.security.cert.Certificate; 40 import java.util.Arrays; 41 import java.util.concurrent.BlockingQueue; 42 import java.util.concurrent.LinkedBlockingQueue; 43 import java.util.concurrent.TimeUnit; 44 45 46 /** 47 * Implements the logic of and holds state for a single occurrence of the 48 * pairing protocol. 49 * <p> 50 * This abstract class implements the logic common to both client and server 51 * perspectives of the protocol. Notably, the 'pairing' phase of the 52 * protocol has the same logic regardless of client/server status 53 * ({link PairingSession#doPairingPhase()}). Other phases of the protocol are 54 * specific to client/server status; see {@link ServerPairingSession} and 55 * {@link ClientPairingSession}. 56 * <p> 57 * The protocol is initiated by called 58 * {@link PairingSession#doPair(PairingListener)} 59 * The listener implementation is responsible for showing the shared secret 60 * to the user 61 * ({@link PairingListener#onPerformOutputDeviceRole(PairingSession, byte[])}), 62 * or in accepting the user input 63 * ({@link PairingListener#onPerformInputDeviceRole(PairingSession)}), 64 * depending on the role negotiated during initialization. 65 * <p> 66 * When operating in the input role, the session will block execution after 67 * calling {@link PairingListener#onPerformInputDeviceRole(PairingSession)} to 68 * wait for the secret. The listener, or some activity resulting from it, must 69 * publish the input secret to the session via 70 * {@link PairingSession#setSecret(byte[])}. 71 */ 72 public abstract class PairingSession { 73 74 protected enum ProtocolState { 75 STATE_UNINITIALIZED, 76 STATE_INITIALIZING, 77 STATE_CONFIGURING, 78 STATE_PAIRING, 79 STATE_SUCCESS, 80 STATE_FAILURE, 81 } 82 83 /** 84 * Enable extra verbose debug logging. 85 */ 86 private static final boolean DEBUG_VERBOSE = false; 87 88 /** 89 * Controls whether to verify the secret portion of the SecretAck message. 90 * <p> 91 * NOTE(mikey): One implementation does not send the secret back in 92 * the SecretAck. This should be fixed, but in the meantime it is not 93 * essential that we verify it, since *any* acknowledgment from the 94 * sender is enough to indicate protocol success. 95 */ 96 private static final boolean VERIFY_SECRET_ACK = false; 97 98 /** 99 * Timeout, in milliseconds, for polling the secret queue for a response from 100 * the listener. This timeout is relevant only to periodically check the 101 * mAbort flag to terminate the protocol, which is set by calling teardown(). 102 */ 103 private static final int SECRET_POLL_TIMEOUT_MS = 500; 104 105 /** 106 * Performs the initialization phase of the protocol. 107 * 108 * @throws PoloException if a protocol error occurred 109 * @throws IOException if an error occurred in input/output 110 */ doInitializationPhase()111 protected abstract void doInitializationPhase() 112 throws PoloException, IOException; 113 114 /** 115 * Performs the configuration phase of the protocol. 116 * 117 * @throws PoloException if a protocol error occurred 118 * @throws IOException if an error occurred in input/output 119 */ doConfigurationPhase()120 protected abstract void doConfigurationPhase() 121 throws PoloException, IOException; 122 123 /** 124 * Internal representation of challenge-response. 125 */ 126 protected PoloChallengeResponse mChallenge; 127 128 /** 129 * Implementation of the transport layer. 130 */ 131 private final PoloWireInterface mProtocol; 132 133 /** 134 * Context for the pairing session. 135 */ 136 protected final PairingContext mPairingContext; 137 138 /** 139 * Local endpoint's supported options. 140 * <p> 141 * If this session is acting as a server, this message will be sent to the 142 * client in the Initialization phase. If acting as a client, this member is 143 * used to store local options and compute the Configuration message (but 144 * is never transmitted directly). 145 */ 146 protected OptionsMessage mLocalOptions; 147 148 /** 149 * Encoding scheme used for the session. 150 */ 151 protected SecretEncoder mEncoder; 152 153 /** 154 * Name of the service being paired. 155 */ 156 protected String mServiceName; 157 158 /** 159 * Name of the peer. 160 */ 161 protected String mPeerName; 162 163 /** 164 * Configuration message for current session. 165 * <p> 166 * This is computed by the client and sent to the server. 167 */ 168 protected ConfigurationMessage mSessionConfig; 169 170 /** 171 * Listener that will receive callbacks upon protocol events. 172 */ 173 protected PairingListener mListener; 174 175 /** 176 * Internal state of the pairing session. 177 */ 178 protected ProtocolState mState; 179 180 /** 181 * Threadsafe queue for receiving the messages sent by peer, user-given secret 182 * from the listener, or exceptions caught by async threads. 183 */ 184 protected BlockingQueue<QueueMessage> mMessageQueue; 185 186 /** 187 * Flag set when the session should be aborted. 188 */ 189 protected boolean mAbort; 190 191 /** 192 * Reader thread. 193 */ 194 private final Thread mThread; 195 196 /** 197 * Constructor. 198 * 199 * @param protocol the wire interface to operate against 200 * @param pairingContext a PairingContext for the session 201 */ PairingSession(PoloWireInterface protocol, PairingContext pairingContext)202 public PairingSession(PoloWireInterface protocol, 203 PairingContext pairingContext) { 204 mProtocol = protocol; 205 mPairingContext = pairingContext; 206 mState = ProtocolState.STATE_UNINITIALIZED; 207 mMessageQueue = new LinkedBlockingQueue<QueueMessage>(); 208 209 Certificate clientCert = mPairingContext.getClientCertificate(); 210 Certificate serverCert = mPairingContext.getServerCertificate(); 211 212 mChallenge = new PoloChallengeResponse(clientCert, serverCert, 213 new PoloChallengeResponse.DebugLogger() { 214 public void debug(String message) { 215 logDebug(message); 216 } 217 public void verbose(String message) { 218 if (DEBUG_VERBOSE) { 219 logDebug(message); 220 } 221 } 222 }); 223 224 mLocalOptions = new OptionsMessage(); 225 226 if (mPairingContext.isServer()) { 227 mLocalOptions.setProtocolRolePreference(ProtocolRole.DISPLAY_DEVICE); 228 } else { 229 mLocalOptions.setProtocolRolePreference(ProtocolRole.INPUT_DEVICE); 230 } 231 232 mThread = new Thread(new Runnable() { 233 public void run() { 234 logDebug("Starting reader"); 235 try { 236 while (!mAbort) { 237 try { 238 PoloMessage message = mProtocol.getNextMessage(); 239 logDebug("Received: " + message.getClass()); 240 mMessageQueue.put(new QueueMessage(message)); 241 } catch (PoloException exception) { 242 logDebug("Exception while getting message: " + exception); 243 mMessageQueue.put(new QueueMessage(exception)); 244 break; 245 } catch (IOException exception) { 246 logDebug("Exception while getting message: " + exception); 247 mMessageQueue.put(new QueueMessage(new PoloException(exception))); 248 break; 249 } 250 } 251 } catch (InterruptedException ie) { 252 logDebug("Interrupted: " + ie); 253 } finally { 254 logDebug("Reader is done"); 255 } 256 } 257 }); 258 mThread.start(); 259 } 260 teardown()261 public void teardown() { 262 try { 263 // Send any error. 264 mProtocol.sendErrorMessage(new Exception()); 265 mPairingContext.getPeerInputStream().close(); 266 mPairingContext.getPeerOutputStream().close(); 267 } catch (IOException e) { 268 // oh well. 269 } 270 271 // Unblock the blocking wait on the secret queue. 272 mAbort = true; 273 mThread.interrupt(); 274 } 275 log(LogLevel level, String message)276 protected void log(LogLevel level, String message) { 277 if (mListener != null) { 278 mListener.onLogMessage(level, message); 279 } 280 } 281 282 /** 283 * Logs a debug message to the active listener. 284 */ logDebug(String message)285 public void logDebug(String message) { 286 log(LogLevel.LOG_DEBUG, message); 287 } 288 289 /** 290 * Logs an informational message to the active listener. 291 */ logInfo(String message)292 public void logInfo(String message) { 293 log(LogLevel.LOG_INFO, message); 294 } 295 296 /** 297 * Logs an error message to the active listener. 298 */ logError(String message)299 public void logError(String message) { 300 log(LogLevel.LOG_ERROR, message); 301 } 302 303 /** 304 * Adds an encoding to the supported input role encodings. This method can 305 * only be called before the session has started. 306 * <p> 307 * If no input encodings have been added, then this endpoint cannot act as 308 * the input device protocol role. 309 * 310 * @param encoding the {@link EncodingOption} to add 311 */ addInputEncoding(EncodingOption encoding)312 public void addInputEncoding(EncodingOption encoding) { 313 if (mState != ProtocolState.STATE_UNINITIALIZED) { 314 throw new IllegalStateException("Cannot add encodings once session " + 315 "has been started."); 316 } 317 // Legal values of GAMMALEN must be: 318 // - an even number of bytes 319 // - at least 2 bytes 320 if ((encoding.getSymbolLength() < 2) || 321 ((encoding.getSymbolLength() % 2) != 0)) { 322 throw new IllegalArgumentException("Bad symbol length: " + 323 encoding.getSymbolLength()); 324 } 325 mLocalOptions.addInputEncoding(encoding); 326 } 327 328 /** 329 * Adds an encoding to the supported output role encodings. This method can 330 * only be called before the session has started. 331 * <p> 332 * If no output encodings have been added, then this endpoint cannot act as 333 * the output device protocol role. 334 * 335 * @param encoding the {@link EncodingOption} to add 336 */ addOutputEncoding(EncodingOption encoding)337 public void addOutputEncoding(EncodingOption encoding) { 338 if (mState != ProtocolState.STATE_UNINITIALIZED) { 339 throw new IllegalStateException("Cannot add encodings once session " + 340 "has been started."); 341 } 342 mLocalOptions.addOutputEncoding(encoding); 343 } 344 345 /** 346 * Changes the internal state. 347 * 348 * @param newState the new state 349 */ setState(ProtocolState newState)350 private void setState(ProtocolState newState) { 351 logInfo("New state: " + newState); 352 mState = newState; 353 } 354 355 /** 356 * Runs the pairing protocol. 357 * <p> 358 * Supported input and output encodings must be specified 359 * first, using 360 * {@link PairingSession#addInputEncoding(EncodingOption)} and 361 * {@link PairingSession#addOutputEncoding(EncodingOption)}, 362 * respectively. 363 * 364 * @param listener the {@link PairingListener} for the session 365 * @return {@code true} if pairing was successful 366 */ doPair(PairingListener listener)367 public boolean doPair(PairingListener listener) { 368 mListener = listener; 369 mListener.onSessionCreated(this); 370 371 if (mPairingContext.isServer()) { 372 logDebug("Protocol started (SERVER mode)"); 373 } else { 374 logDebug("Protocol started (CLIENT mode)"); 375 } 376 377 logDebug("Local options: " + mLocalOptions.toString()); 378 379 Certificate clientCert = mPairingContext.getClientCertificate(); 380 if (DEBUG_VERBOSE) { 381 logDebug("Client certificate:"); 382 logDebug(clientCert.toString()); 383 } 384 385 Certificate serverCert = mPairingContext.getServerCertificate(); 386 387 if (DEBUG_VERBOSE) { 388 logDebug("Server certificate:"); 389 logDebug(serverCert.toString()); 390 } 391 392 boolean success = false; 393 394 try { 395 setState(ProtocolState.STATE_INITIALIZING); 396 doInitializationPhase(); 397 398 setState(ProtocolState.STATE_CONFIGURING); 399 doConfigurationPhase(); 400 401 setState(ProtocolState.STATE_PAIRING); 402 doPairingPhase(); 403 404 success = true; 405 } catch (ProtocolErrorException e) { 406 logDebug("Remote protocol failure: " + e); 407 } catch (PoloException e) { 408 try { 409 logDebug("Local protocol failure, attempting to send error: " + e); 410 mProtocol.sendErrorMessage(e); 411 } catch (IOException e1) { 412 logDebug("Error message send failed"); 413 } 414 } catch (IOException e) { 415 logDebug("IOException: " + e); 416 } 417 418 if (success) { 419 setState(ProtocolState.STATE_SUCCESS); 420 } else { 421 setState(ProtocolState.STATE_FAILURE); 422 } 423 424 mListener.onSessionEnded(this); 425 return success; 426 } 427 428 /** 429 * Returns {@code true} if the session is in a terminal state (success or 430 * failure). 431 */ hasCompleted()432 public boolean hasCompleted() { 433 switch (mState) { 434 case STATE_SUCCESS: 435 case STATE_FAILURE: 436 return true; 437 default: 438 return false; 439 } 440 } 441 hasSucceeded()442 public boolean hasSucceeded() { 443 return mState == ProtocolState.STATE_SUCCESS; 444 } 445 getServiceName()446 public String getServiceName() { 447 return mServiceName; 448 } 449 450 /** 451 * Sets the secret, as received from a user. This method is only meaningful 452 * when the endpoint is acting as the input device role. 453 * 454 * @param secret the secret, as a byte sequence 455 * @return {@code true} if the secret was captured 456 */ setSecret(byte[] secret)457 public boolean setSecret(byte[] secret) { 458 if (!isInputDevice()) { 459 throw new IllegalStateException("Secret can only be set for " + 460 "input role session."); 461 } else if (mState != ProtocolState.STATE_PAIRING) { 462 throw new IllegalStateException("Secret can only be set while " + 463 "in pairing state."); 464 } 465 return mMessageQueue.offer(new QueueMessage(secret)); 466 } 467 468 /** 469 * Executes the pairing phase of the protocol. 470 * 471 * @throws PoloException if a protocol error occurred 472 * @throws IOException if an error in the input/output occurred 473 */ doPairingPhase()474 protected void doPairingPhase() throws PoloException, IOException { 475 if (isInputDevice()) { 476 new Thread(new Runnable() { 477 public void run() { 478 logDebug("Calling listener for user input..."); 479 try { 480 mListener.onPerformInputDeviceRole(PairingSession.this); 481 } catch (PoloException exception) { 482 logDebug("Sending exception: " + exception); 483 mMessageQueue.offer(new QueueMessage(exception)); 484 } finally { 485 logDebug("Listener finished."); 486 } 487 } 488 }).start(); 489 490 logDebug("Waiting for secret from Listener or ..."); 491 QueueMessage message = waitForMessage(); 492 if (message == null || !message.hasSecret()) { 493 throw new PoloException( 494 "Illegal state - no secret available: " + message); 495 } 496 byte[] userGamma = message.mSecret; 497 if (userGamma == null) { 498 throw new PoloException("Invalid secret."); 499 } 500 501 boolean match = mChallenge.checkGamma(userGamma); 502 if (match != true) { 503 throw new BadSecretException("Secret failed local check."); 504 } 505 506 byte[] userNonce = mChallenge.extractNonce(userGamma); 507 byte[] genAlpha = mChallenge.getAlpha(userNonce); 508 509 logDebug("Sending Secret reply..."); 510 SecretMessage secretMessage = new SecretMessage(genAlpha); 511 mProtocol.sendMessage(secretMessage); 512 513 logDebug("Waiting for SecretAck..."); 514 SecretAckMessage secretAck = 515 (SecretAckMessage) getNextMessage(PoloMessageType.SECRET_ACK); 516 517 if (VERIFY_SECRET_ACK) { 518 byte[] inbandAlpha = secretAck.getSecret(); 519 if (!Arrays.equals(inbandAlpha, genAlpha)) { 520 throw new BadSecretException("Inband secret did not match. " + 521 "Expected [" + PoloUtil.bytesToHexString(genAlpha) + 522 "], got [" + PoloUtil.bytesToHexString(inbandAlpha) + "]"); 523 } 524 } 525 } else { 526 int symbolLength = mSessionConfig.getEncoding().getSymbolLength(); 527 int nonceLength = symbolLength / 2; 528 int bytesNeeded = nonceLength / mEncoder.symbolsPerByte(); 529 530 byte[] nonce = new byte[bytesNeeded]; 531 SecureRandom random; 532 try { 533 random = SecureRandom.getInstance("SHA1PRNG"); 534 } catch (NoSuchAlgorithmException e) { 535 throw new PoloException(e); 536 } 537 random.nextBytes(nonce); 538 539 // Display gamma 540 logDebug("Calling listener to display output..."); 541 byte[] gamma = mChallenge.getGamma(nonce); 542 mListener.onPerformOutputDeviceRole(this, gamma); 543 544 logDebug("Waiting for Secret..."); 545 SecretMessage secretMessage = 546 (SecretMessage) getNextMessage(PoloMessageType.SECRET); 547 548 byte[] localAlpha = mChallenge.getAlpha(nonce); 549 byte[] inbandAlpha = secretMessage.getSecret(); 550 boolean matched = Arrays.equals(localAlpha, inbandAlpha); 551 552 if (!matched) { 553 throw new BadSecretException("Inband secret did not match. " + 554 "Expected [" + PoloUtil.bytesToHexString(localAlpha) + 555 "], got [" + PoloUtil.bytesToHexString(inbandAlpha) + "]"); 556 } 557 558 logDebug("Sending SecretAck..."); 559 byte[] genAlpha = mChallenge.getAlpha(nonce); 560 SecretAckMessage secretAck = new SecretAckMessage(inbandAlpha); 561 mProtocol.sendMessage(secretAck); 562 } 563 } 564 getEncoder()565 public SecretEncoder getEncoder() { 566 return mEncoder; 567 } 568 569 /** 570 * Sets the current session's configuration from a 571 * {@link ConfigurationMessage}. 572 * 573 * @param message the session's config 574 * @throws PoloException if the config was not valid for some reason 575 */ setConfiguration(ConfigurationMessage message)576 protected void setConfiguration(ConfigurationMessage message) 577 throws PoloException { 578 if (message == null || message.getEncoding() == null) { 579 throw new NoConfigurationException("No configuration is possible."); 580 } 581 if (message.getEncoding().getSymbolLength() % 2 != 0) { 582 throw new PoloException("Symbol length must be even."); 583 } 584 if (message.getEncoding().getSymbolLength() < 2) { 585 throw new PoloException("Symbol length must be >= 2 symbols."); 586 } 587 switch (message.getEncoding().getType()) { 588 case ENCODING_HEXADECIMAL: 589 mEncoder = new HexadecimalEncoder(); 590 break; 591 default: 592 throw new PoloException("Unsupported encoding type."); 593 } 594 mSessionConfig = message; 595 } 596 597 /** 598 * Returns the role of this endpoint in the current session. 599 */ getLocalRole()600 protected ProtocolRole getLocalRole() { 601 assert (mSessionConfig != null); 602 if (!mPairingContext.isServer()) { 603 return mSessionConfig.getClientRole(); 604 } else { 605 return (mSessionConfig.getClientRole() == ProtocolRole.DISPLAY_DEVICE) ? 606 ProtocolRole.INPUT_DEVICE : ProtocolRole.DISPLAY_DEVICE; 607 } 608 } 609 610 /** 611 * Returns {@code true} if this endpoint will act as the input device. 612 */ isInputDevice()613 protected boolean isInputDevice() { 614 return (getLocalRole() == ProtocolRole.INPUT_DEVICE); 615 } 616 617 /** 618 * Returns {@code true} if peer's name is set. 619 */ hasPeerName()620 public boolean hasPeerName() { 621 return mPeerName != null; 622 } 623 624 /** 625 * Returns peer's name if set, {@code null} otherwise. 626 */ getPeerName()627 public String getPeerName() { 628 return mPeerName; 629 } 630 getNextMessage(PoloMessageType type)631 protected PoloMessage getNextMessage(PoloMessageType type) 632 throws PoloException { 633 QueueMessage message = waitForMessage(); 634 if (message != null && message.hasPoloMessage()) { 635 if (!type.equals(message.mPoloMessage.getType())) { 636 throw new PoloException( 637 "Unexpected message type: " + message.mPoloMessage.getType()); 638 } 639 return message.mPoloMessage; 640 } 641 throw new PoloException("Invalid state - expected polo message"); 642 } 643 644 /** 645 * Returns next queued message. The method blocks until the secret or the 646 * polo message is available. 647 * 648 * @return the queued message, or null on error 649 * @throws PoloException if exception was queued 650 */ waitForMessage()651 private QueueMessage waitForMessage() throws PoloException { 652 while (!mAbort) { 653 try { 654 QueueMessage message = mMessageQueue.poll(SECRET_POLL_TIMEOUT_MS, 655 TimeUnit.MILLISECONDS); 656 657 if (message != null) { 658 if (message.hasPoloException()) { 659 throw new PoloException(message.mPoloException); 660 } 661 return message; 662 } 663 } catch (InterruptedException e) { 664 break; 665 } 666 } 667 668 // Aborted or interrupted. 669 return null; 670 } 671 672 /** 673 * Sends message to the peer. 674 * 675 * @param message the message 676 * @throws PoloException if a protocol error occurred 677 * @throws IOException if an error in the input/output occurred 678 */ sendMessage(PoloMessage message)679 protected void sendMessage(PoloMessage message) 680 throws IOException, PoloException { 681 mProtocol.sendMessage(message); 682 } 683 684 /** 685 * Queued message, that can carry information about secret, next read message, 686 * or exception caught by reader or input threads. 687 */ 688 private static final class QueueMessage { 689 final PoloMessage mPoloMessage; 690 final PoloException mPoloException; 691 final byte[] mSecret; 692 QueueMessage( PoloMessage message, byte[] secret, PoloException exception)693 private QueueMessage( 694 PoloMessage message, byte[] secret, PoloException exception) { 695 int nonNullCount = 0; 696 if (message != null) { 697 ++nonNullCount; 698 } 699 mPoloMessage = message; 700 if (exception != null) { 701 assert(nonNullCount == 0); 702 ++nonNullCount; 703 } 704 mPoloException = exception; 705 if (secret != null) { 706 assert(nonNullCount == 0); 707 ++nonNullCount; 708 } 709 mSecret = secret; 710 assert(nonNullCount == 1); 711 } 712 QueueMessage(PoloMessage message)713 public QueueMessage(PoloMessage message) { 714 this(message, null, null); 715 } 716 QueueMessage(byte[] secret)717 public QueueMessage(byte[] secret) { 718 this(null, secret, null); 719 } 720 QueueMessage(PoloException exception)721 public QueueMessage(PoloException exception) { 722 this(null, null, exception); 723 } 724 hasPoloMessage()725 public boolean hasPoloMessage() { 726 return mPoloMessage != null; 727 } 728 hasPoloException()729 public boolean hasPoloException() { 730 return mPoloException != null; 731 } 732 hasSecret()733 public boolean hasSecret() { 734 return mSecret != null; 735 } 736 737 @Override toString()738 public String toString() { 739 StringBuilder builder = new StringBuilder("QueueMessage("); 740 if (hasPoloMessage()) { 741 builder.append("poloMessage = " + mPoloMessage); 742 } 743 if (hasPoloException()) { 744 builder.append("poloException = " + mPoloException); 745 } 746 if (hasSecret()) { 747 builder.append("secret = " + Arrays.toString(mSecret)); 748 } 749 return builder.append(")").toString(); 750 } 751 } 752 753 } 754