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