1 /* 2 * Copyright (C) 2021 The Android Open Source Project 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 package com.android.server.uwb.secure; 17 18 import static com.android.server.uwb.secure.csml.DispatchResponse.NOTIFICATION_EVENT_ID_ADF_SELECTED; 19 import static com.android.server.uwb.secure.csml.DispatchResponse.NOTIFICATION_EVENT_ID_RDS_AVAILABLE; 20 import static com.android.server.uwb.secure.csml.DispatchResponse.NOTIFICATION_EVENT_ID_SECURE_CHANNEL_ESTABLISHED; 21 import static com.android.server.uwb.secure.csml.DispatchResponse.NOTIFICATION_EVENT_ID_SECURE_SESSION_ABORTED; 22 import static com.android.server.uwb.secure.iso7816.StatusWord.SW_NO_ERROR; 23 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.Message; 27 import android.util.Log; 28 29 import androidx.annotation.NonNull; 30 import androidx.annotation.WorkerThread; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.server.uwb.discovery.Transport; 34 import com.android.server.uwb.discovery.info.FiraConnectorMessage.MessageType; 35 import com.android.server.uwb.pm.RunningProfileSessionInfo; 36 import com.android.server.uwb.secure.csml.CsmlUtil; 37 import com.android.server.uwb.secure.csml.DispatchCommand; 38 import com.android.server.uwb.secure.csml.DispatchResponse; 39 import com.android.server.uwb.secure.csml.FiRaCommand; 40 import com.android.server.uwb.secure.csml.GetDoCommand; 41 import com.android.server.uwb.secure.csml.GetDoResponse; 42 import com.android.server.uwb.secure.csml.SwapInAdfCommand; 43 import com.android.server.uwb.secure.csml.SwapInAdfResponse; 44 import com.android.server.uwb.secure.csml.SwapOutAdfCommand; 45 import com.android.server.uwb.secure.csml.SwapOutAdfResponse; 46 import com.android.server.uwb.secure.iso7816.CommandApdu; 47 import com.android.server.uwb.secure.iso7816.ResponseApdu; 48 import com.android.server.uwb.secure.iso7816.TlvDatum; 49 import com.android.server.uwb.secure.iso7816.TlvParser; 50 import com.android.server.uwb.util.DataTypeConversionUtil; 51 import com.android.server.uwb.util.ObjectIdentifier; 52 53 import java.io.IOException; 54 import java.util.Objects; 55 import java.util.Optional; 56 57 /** 58 * Set up the secure channel and handle the Tunnel data request. 59 * For Tunnel data, simplex from Initiator is support. as the 'DISPATCH' limitation. 60 */ 61 @WorkerThread 62 public abstract class FiRaSecureChannel { 63 private static final String LOG_TAG = "FiRaSecureChannel"; 64 65 private final Transport mTransport; 66 protected final SecureElementChannel mSecureElementChannel; 67 protected final RunningProfileSessionInfo mRunningProfileSessionInfo; 68 protected SecureChannelCallback mSecureChannelCallback; 69 @VisibleForTesting final Handler mWorkHandler; 70 71 enum SetupError { 72 INIT, 73 SELECT_ADF, 74 SWAP_IN_ADF, 75 INITIATE_TRANSACTION, 76 OPEN_SE_CHANNEL, 77 DISPATCH, 78 ADF_NOT_MATCHED, 79 } 80 81 enum Status { 82 UNINITIALIZED, 83 INITIALIZED, 84 CHANNEL_OPENED, 85 ADF_SELECTED, 86 ESTABLISHED, 87 TERMINATED, 88 ABNORMAL, 89 } 90 91 static final int CMD_INIT = 0; 92 static final int CMD_OPEN_CHANNEL = 1; 93 static final int CMD_SELECT_ADF = 2; 94 static final int CMD_INITIATE_TRANSACTION = 3; 95 static final int CMD_SEND_OOB_DATA = 4; 96 static final int CMD_PROCESS_RECEIVED_OOB_DATA = 5; 97 static final int CMD_CLEAN_UP_TERMINATED_OR_ABORTED_CHANNEL = 6; 98 99 static final int OOB_MSG_TYPE_APDU_COMMAND = 0; 100 static final int OOB_MSG_TYPE_APDU_RESPONSE = 1; 101 102 protected Status mStatus = Status.UNINITIALIZED; 103 private Optional<byte[]> mDynamicSlotIdentifier = Optional.empty(); 104 FiRaSecureChannel( @onNull SecureElementChannel secureElementChannel, @NonNull Transport transport, @NonNull Looper workLooper, @NonNull RunningProfileSessionInfo runningProfileSessionInfo)105 FiRaSecureChannel( 106 @NonNull SecureElementChannel secureElementChannel, 107 @NonNull Transport transport, 108 @NonNull Looper workLooper, 109 @NonNull RunningProfileSessionInfo runningProfileSessionInfo) { 110 this.mSecureElementChannel = secureElementChannel; 111 this.mTransport = transport; 112 this.mWorkHandler = 113 new Handler(workLooper) { 114 @Override 115 public void handleMessage(Message msg) { 116 handleScMessage(msg); 117 } 118 }; 119 this.mRunningProfileSessionInfo = runningProfileSessionInfo; 120 } 121 122 private final Transport.DataReceiver mDataReceiver = 123 new Transport.DataReceiver() { 124 @Override 125 public void onDataReceived(@NonNull byte[] data) { 126 mWorkHandler.sendMessage( 127 mWorkHandler.obtainMessage(CMD_PROCESS_RECEIVED_OOB_DATA, data)); 128 } 129 }; 130 handleScMessage(@onNull Message msg)131 protected void handleScMessage(@NonNull Message msg) { 132 switch (msg.what) { 133 case CMD_INIT: 134 mSecureElementChannel.init( 135 () -> { 136 // do nothing for ROLE_RESPONDER, wait cmd from remote device 137 if (doOpenSeChannelAfterInit()) { 138 mWorkHandler.sendMessage( 139 mWorkHandler.obtainMessage(CMD_OPEN_CHANNEL)); 140 } 141 142 mTransport.registerDataReceiver(mDataReceiver); 143 mStatus = Status.INITIALIZED; 144 }); 145 break; 146 case CMD_SEND_OOB_DATA: 147 byte[] payload = (byte[]) msg.obj; 148 int msgType = msg.arg1; 149 MessageType firaMsgType = 150 msgType == OOB_MSG_TYPE_APDU_COMMAND 151 ? MessageType.COMMAND : MessageType.COMMAND_RESPOND; 152 mTransport.sendData( 153 firaMsgType, 154 payload, 155 new Transport.SendingDataCallback() { 156 @Override 157 public void onSuccess() { 158 // do nothing 159 } 160 161 @Override 162 public void onFailure() { 163 // TODO: retry to send it, end the session if it is failed many 164 // times. 165 } 166 }); 167 break; 168 case CMD_PROCESS_RECEIVED_OOB_DATA: 169 byte[] receivedData = (byte[]) msg.obj; 170 processRemoteCommandOrResponse(receivedData); 171 break; 172 case CMD_CLEAN_UP_TERMINATED_OR_ABORTED_CHANNEL: 173 mDynamicSlotIdentifier.ifPresent((slotId) -> swapOutAdf(slotId)); 174 175 if (mSecureElementChannel.closeChannel()) { 176 mStatus = Status.INITIALIZED; 177 mSecureChannelCallback.onSeChannelClosed(/*withError=*/ false); 178 } else { 179 logw("error happened on closing SE channel"); 180 mStatus = Status.ABNORMAL; 181 mSecureChannelCallback.onSeChannelClosed(/*withError=*/ true); 182 } 183 184 break; 185 } 186 } 187 doOpenSeChannelAfterInit()188 protected abstract boolean doOpenSeChannelAfterInit(); 189 190 /** 191 * Initiate the secure session set up. 192 */ init(@onNull SecureChannelCallback secureChannelCallback)193 public void init(@NonNull SecureChannelCallback secureChannelCallback) { 194 if (mStatus == Status.ABNORMAL) { 195 throw new IllegalStateException("fatal error, the session should be discarded"); 196 } 197 mWorkHandler.sendMessage(mWorkHandler.obtainMessage(CMD_INIT)); 198 mSecureChannelCallback = secureChannelCallback; 199 } 200 201 /** 202 * Swap in the ADF, this is optional, used only when the service profile is using the 203 * dynamic slot. 204 * @param secureBlob The secure BLOB contains the ADF OID and its encrypted content. 205 */ swapInAdf( @onNull byte[] secureBlob, @NonNull ObjectIdentifier adfOid, @NonNull byte[] uwbControleeInfo)206 protected final boolean swapInAdf( 207 @NonNull byte[] secureBlob, 208 @NonNull ObjectIdentifier adfOid, 209 @NonNull byte[] uwbControleeInfo) { 210 SwapInAdfCommand swapInAdfCmd = 211 SwapInAdfCommand.build(secureBlob, adfOid, uwbControleeInfo); 212 try { 213 SwapInAdfResponse response = 214 SwapInAdfResponse.fromResponseApdu( 215 mSecureElementChannel.transmit(swapInAdfCmd)); 216 if (!response.isSuccess() || response.slotIdentifier.isEmpty()) { 217 throw new IllegalStateException(response.statusWord.toString()); 218 } else { 219 mDynamicSlotIdentifier = response.slotIdentifier; 220 return true; 221 } 222 } catch (IOException | IllegalStateException e) { 223 logw("error on swapping in ADF: " + e); 224 } 225 return false; 226 } 227 swapOutAdf(@onNull byte[] slotIdentifier)228 private boolean swapOutAdf(@NonNull byte[] slotIdentifier) { 229 SwapOutAdfCommand swapOutAdfCmd = SwapOutAdfCommand.build(slotIdentifier); 230 try { 231 SwapOutAdfResponse response = 232 SwapOutAdfResponse.fromResponseApdu( 233 mSecureElementChannel.transmit(swapOutAdfCmd)); 234 if (!response.isSuccess()) { 235 throw new IllegalStateException(response.statusWord.toString()); 236 } 237 mDynamicSlotIdentifier = Optional.empty(); 238 } catch (IOException | IllegalStateException e) { 239 logw("Failed to swap out ADF with exception: " + e); 240 return false; 241 } 242 return true; 243 } 244 preprocessRemoteCommand(@onNull byte[] data)245 protected boolean preprocessRemoteCommand(@NonNull byte[] data) { 246 return false; 247 } 248 249 @VisibleForTesting processRemoteCommandOrResponse(@onNull byte[] data)250 void processRemoteCommandOrResponse(@NonNull byte[] data) { 251 if (preprocessRemoteCommand(data)) { 252 return; 253 } 254 255 try { 256 if (!mSecureElementChannel.isOpened()) { 257 throw new IllegalStateException("the SE is not opened to handle command."); 258 } 259 // otherwise, dispatch to FiRa applet 260 DispatchCommand dispatchCommand = DispatchCommand.build(data); 261 DispatchResponse response = 262 DispatchResponse.fromResponseApdu( 263 mSecureElementChannel.transmit(dispatchCommand)); 264 if (mStatus == Status.ESTABLISHED) { 265 // send to initiator or responder 266 mSecureChannelCallback.onDispatchResponseAvailable(response); 267 } else { 268 if (!response.isSuccess()) { 269 throw new IllegalStateException( 270 "Dispatch Command error: " + response.statusWord); 271 } 272 handleDispatchResponseForSc(response); 273 } 274 } catch (IOException | IllegalStateException e) { 275 logw("Dispatch command failed for " + e); 276 if (mStatus != Status.ESTABLISHED) { 277 mSecureChannelCallback.onSetUpError(SetupError.DISPATCH); 278 ResponseApdu responseApdu = ResponseApdu.SW_CONDITIONS_NOT_SATISFIED_APDU; 279 mWorkHandler.sendMessage( 280 mWorkHandler.obtainMessage(CMD_SEND_OOB_DATA, responseApdu.toByteArray())); 281 } else { 282 // send the error to initiator or responder. 283 mSecureChannelCallback.onDispatchCommandFailure(); 284 } 285 } 286 } 287 handleDispatchResponseForSc(@onNull DispatchResponse dispatchResponse)288 private void handleDispatchResponseForSc(@NonNull DispatchResponse dispatchResponse) { 289 Optional<DispatchResponse.OutboundData> outboundData = dispatchResponse.getOutboundData(); 290 if (outboundData.isPresent()) { 291 if (outboundData.get().target == DispatchResponse.OUTBOUND_TARGET_REMOTE) { 292 mWorkHandler.sendMessage( 293 mWorkHandler.obtainMessage(CMD_SEND_OOB_DATA, outboundData.get().data)); 294 } else { 295 if (mStatus != Status.ESTABLISHED) { 296 logw( 297 "Session set up, ignore data to host, dup as SW " 298 + DataTypeConversionUtil.byteArrayToHexString( 299 outboundData.get().data)); 300 } 301 } 302 } 303 for (DispatchResponse.Notification notification : dispatchResponse.notifications) { 304 switch (notification.notificationEventId) { 305 case NOTIFICATION_EVENT_ID_ADF_SELECTED: 306 logd("ADF selected"); 307 DispatchResponse.AdfSelectedNotification adfSelected = 308 (DispatchResponse.AdfSelectedNotification) notification; 309 ObjectIdentifier selectedAdfOid = adfSelected.adfOid; 310 if (!mRunningProfileSessionInfo.oidOfProvisionedAdf 311 .equals(adfSelected.adfOid)) { 312 logw("The selected ADF doesn't match the provisioned ADF."); 313 mSecureChannelCallback.onSetUpError(SetupError.ADF_NOT_MATCHED); 314 } else { 315 mStatus = Status.ADF_SELECTED; 316 } 317 break; 318 case NOTIFICATION_EVENT_ID_SECURE_CHANNEL_ESTABLISHED: 319 logd("SC established"); 320 mStatus = Status.ESTABLISHED; 321 DispatchResponse.SecureChannelEstablishedNotification eNotification = 322 (DispatchResponse.SecureChannelEstablishedNotification) notification; 323 logd("defaultSessionId from notification: " 324 + eNotification.defaultSessionId); 325 Optional<Integer> defaultSessionId = Optional.empty(); 326 if (eNotification.defaultSessionId.isEmpty()) { 327 defaultSessionId = readDefaultSessionId(); 328 } 329 mSecureChannelCallback.onEstablished(defaultSessionId); 330 break; 331 case NOTIFICATION_EVENT_ID_SECURE_SESSION_ABORTED: 332 cleanUpTerminatedOrAbortedSession(); 333 break; 334 case NOTIFICATION_EVENT_ID_RDS_AVAILABLE: 335 logd("RDS available and SC terminated automatically"); 336 // see CSML 8.2.2.7.1.8 Table 64 - ADF Extended Options 337 // RDS available means the session is using the default session id and key 338 // Also the secure channel is terminated automatically. 339 DispatchResponse.RdsAvailableNotification rdsAvailableNotification = 340 (DispatchResponse.RdsAvailableNotification) notification; 341 mStatus = Status.TERMINATED; 342 mSecureChannelCallback.onRdsAvailableAndTerminated( 343 rdsAvailableNotification.sessionId); 344 break; 345 default: 346 logw( 347 "Unexpected notification from dispatch response: " 348 + notification.notificationEventId); 349 } 350 } 351 } 352 readDefaultSessionId()353 private Optional<Integer> readDefaultSessionId() { 354 TlvDatum getSessionIdTlv = CsmlUtil.constructGetSessionIdGetDoTlv(); 355 GetDoCommand getSessionIdCommand = GetDoCommand.build(getSessionIdTlv); 356 try { 357 ResponseApdu responseApdu = 358 mSecureElementChannel.transmit(getSessionIdCommand); 359 if (responseApdu != null && responseApdu.getStatusWord() == SW_NO_ERROR.toInt()) { 360 TlvDatum sessionIdTlv = TlvParser.parseOneTlv(responseApdu.getResponseData()); 361 if (sessionIdTlv != null 362 && Objects.equals(sessionIdTlv.tag, CsmlUtil.SESSION_ID_TAG)) { 363 return Optional.of( 364 DataTypeConversionUtil.arbitraryByteArrayToI32(sessionIdTlv.value)); 365 } 366 } else { 367 throw new IllegalStateException("no valid APDU response."); 368 } 369 } catch (IOException | IllegalStateException e) { 370 logw("error to getSessionId DO."); 371 } 372 return Optional.empty(); 373 } 374 isEstablished()375 boolean isEstablished() { 376 return mStatus == Status.ESTABLISHED; 377 } 378 sendRawDataToRemote(@onNull byte[] data)379 void sendRawDataToRemote(@NonNull byte[] data) { 380 mWorkHandler.sendMessage(mWorkHandler.obtainMessage(CMD_SEND_OOB_DATA, data)); 381 } 382 cleanUpTerminatedOrAbortedSession()383 void cleanUpTerminatedOrAbortedSession() { 384 mWorkHandler.sendMessage( 385 mWorkHandler.obtainMessage(CMD_CLEAN_UP_TERMINATED_OR_ABORTED_CHANNEL)); 386 } 387 sendLocalFiRaCommand( @onNull FiRaCommand fiRaCommand, @NonNull ExternalRequestCallback externalRequestCallback)388 void sendLocalFiRaCommand( 389 @NonNull FiRaCommand fiRaCommand, 390 @NonNull ExternalRequestCallback externalRequestCallback) { 391 sendLocalCommandApdu(fiRaCommand.getCommandApdu(), externalRequestCallback); 392 } 393 394 /** 395 * Send the APDU to the FiRa applet through the channel. 396 */ sendLocalCommandApdu( @onNull CommandApdu commandApdu, @NonNull ExternalRequestCallback externalRequestCallback)397 void sendLocalCommandApdu( 398 @NonNull CommandApdu commandApdu, 399 @NonNull ExternalRequestCallback externalRequestCallback) { 400 mWorkHandler.post( 401 () -> { 402 try { 403 if (!mSecureElementChannel.isOpened()) { 404 throw new IllegalStateException("the OMAPI channel is not opened."); 405 } 406 407 ResponseApdu responseApdu = mSecureElementChannel.transmit(commandApdu); 408 if (responseApdu.getStatusWord() == SW_NO_ERROR.toInt()) { 409 externalRequestCallback.onSuccess(responseApdu.getResponseData()); 410 } else { 411 logw("Applet failed to handle the APDU: " + commandApdu); 412 externalRequestCallback.onFailure(); 413 } 414 } catch (IOException | IllegalStateException e) { 415 logw("sendLocalCommandApdu failed as: " + e); 416 externalRequestCallback.onFailure(); 417 } 418 }); 419 } 420 tunnelToRemoteDevice( @onNull byte[] data, @NonNull ExternalRequestCallback externalRequestCallback)421 abstract void tunnelToRemoteDevice( 422 @NonNull byte[] data, @NonNull ExternalRequestCallback externalRequestCallback); 423 terminateLocally()424 void terminateLocally() { 425 mWorkHandler.post( 426 () -> { 427 if (mStatus != Status.ESTABLISHED) { 428 mSecureChannelCallback.onTerminated(/*withError=*/ false); 429 return; 430 } 431 // send terminate command to SE 432 // send GetDataDO - terminate session to local. 433 TlvDatum terminateSessionDo = CsmlUtil.constructTerminateSessionGetDoTlv(); 434 GetDoCommand getDoCommand = GetDoCommand.build(terminateSessionDo); 435 try { 436 GetDoResponse response = 437 GetDoResponse.fromResponseApdu( 438 mSecureElementChannel.transmit(getDoCommand)); 439 if (response.isSuccess()) { 440 mSecureChannelCallback.onTerminated(/*withError=*/ false); 441 mStatus = Status.TERMINATED; 442 } else { 443 throw new IllegalStateException( 444 "Terminate response error: " + response.statusWord); 445 } 446 } catch (IOException | IllegalStateException e) { 447 logw("Error happened on termination locally: " + e); 448 mStatus = Status.ABNORMAL; 449 mSecureChannelCallback.onTerminated(/*withError=*/ true); 450 } 451 }); 452 } 453 getStatus()454 Status getStatus() { 455 return mStatus; 456 } 457 458 interface SecureChannelCallback { 459 /** 460 * The secure session is set up. Ready to handle secure message exchanging. 461 */ onEstablished(@onNull Optional<Integer> defaultUniqueSessionId)462 void onEstablished(@NonNull Optional<Integer> defaultUniqueSessionId); 463 464 /** 465 * Error happens during the secure session set up. 466 */ onSetUpError(SetupError error)467 void onSetUpError(SetupError error); 468 469 /** 470 * Received DispatchResponse which is for the DispatchCommand 471 * received from the remote device after the secure channel setup. 472 */ onDispatchResponseAvailable(DispatchResponse dispatchResponse)473 void onDispatchResponseAvailable(DispatchResponse dispatchResponse); 474 475 /** 476 * The dispatch command wasn't handled correctly by the applet. 477 */ onDispatchCommandFailure()478 void onDispatchCommandFailure(); 479 480 /** 481 * The Secure channel is terminated as response of TERMINATE command. 482 * If the channel is automatically terminated, this will not be called. 483 */ onTerminated(boolean withError)484 void onTerminated(boolean withError); 485 486 /** 487 * The secure element channel for the session is closed. 488 */ onSeChannelClosed(boolean withError)489 void onSeChannelClosed(boolean withError); 490 491 /** 492 * The session is set up completed and terminated automatically. 493 * 494 * @param sessionId - the uwb session ID derived in the FiRa applet 495 */ onRdsAvailableAndTerminated(int sessionId)496 void onRdsAvailableAndTerminated(int sessionId); 497 } 498 499 interface ExternalRequestCallback { 500 /** 501 * The request is handled correctly. 502 */ onSuccess(@onNull byte[] responseData)503 void onSuccess(@NonNull byte[] responseData); 504 505 /** 506 * The request cannot be handled. 507 */ onFailure()508 void onFailure(); 509 } 510 logw(@onNull String dbgMsg)511 private void logw(@NonNull String dbgMsg) { 512 Log.w(LOG_TAG, dbgMsg); 513 } logd(@onNull String dbgMsg)514 private void logd(@NonNull String dbgMsg) { 515 Log.d(LOG_TAG, dbgMsg); 516 } 517 } 518