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.csml;
17 
18 import android.annotation.IntDef;
19 import android.util.Log;
20 
21 import androidx.annotation.NonNull;
22 import androidx.annotation.Nullable;
23 import androidx.annotation.VisibleForTesting;
24 
25 import com.android.server.uwb.secure.iso7816.ResponseApdu;
26 import com.android.server.uwb.secure.iso7816.TlvDatum;
27 import com.android.server.uwb.secure.iso7816.TlvDatum.Tag;
28 import com.android.server.uwb.secure.iso7816.TlvParser;
29 import com.android.server.uwb.util.DataTypeConversionUtil;
30 import com.android.server.uwb.util.ObjectIdentifier;
31 
32 import java.lang.annotation.Retention;
33 import java.lang.annotation.RetentionPolicy;
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Optional;
38 
39 /**
40  * Response of Dispatch APDU, See CSML 1.0 - 8.2.2.14.2.9
41  */
42 public class DispatchResponse extends FiRaResponse {
43     private static final String LOG_TAG = "DispatchResponse";
44     @VisibleForTesting
45     static final Tag STATUS_TAG = new Tag((byte) 0x80);
46     @VisibleForTesting
47     static final Tag DATA_TAG = new Tag((byte) 0x81);
48     @VisibleForTesting
49     static final Tag NOTIFICATION_TAG = new Tag((byte) 0xE1);
50     @VisibleForTesting
51     static final Tag NOTIFICATION_FORMAT_TAG = new Tag((byte) 0x80);
52     @VisibleForTesting
53     static final Tag NOTIFICATION_EVENT_ID_TAG = new Tag((byte) 0x81);
54     @VisibleForTesting
55     static final Tag NOTIFICATION_DATA_TAG = new Tag((byte) 0x82);
56 
57     @IntDef(prefix = { "TRANSACTION_STATUS_" }, value = {
58             TRANSACTION_STATUS_UNDEFINED,
59             TRANSACTION_STATUS_COMPLETE,
60             TRANSACTION_STATUS_FORWARD_TO_REMOTE,
61             TRANSACTION_STATUS_FORWARD_TO_HOST,
62             TRANSACTION_STATUS_WITH_ERROR,
63     })
64     @Retention(RetentionPolicy.SOURCE)
65     private @interface TransactionStatus {}
66 
67     private static final int TRANSACTION_STATUS_UNDEFINED = -1;
68     private static final int TRANSACTION_STATUS_COMPLETE = 0;
69     private static final int TRANSACTION_STATUS_FORWARD_TO_REMOTE = 1;
70     private static final int TRANSACTION_STATUS_FORWARD_TO_HOST = 2;
71     private static final int TRANSACTION_STATUS_WITH_ERROR = 3;
72 
73 
74     @IntDef(prefix = { "NOTIFICATION_EVENT_ID_" }, value = {
75             NOTIFICATION_EVENT_ID_ADF_SELECTED,
76             NOTIFICATION_EVENT_ID_SECURE_CHANNEL_ESTABLISHED,
77             NOTIFICATION_EVENT_ID_RDS_AVAILABLE,
78             NOTIFICATION_EVENT_ID_SECURE_SESSION_ABORTED,
79             NOTIFICATION_EVENT_ID_CONTROLEE_INFO_AVAILABLE,
80     })
81     @Retention(RetentionPolicy.SOURCE)
82     public @interface NotificationEventId {}
83 
84     public static final int NOTIFICATION_EVENT_ID_ADF_SELECTED = 0;
85     public static final int NOTIFICATION_EVENT_ID_SECURE_CHANNEL_ESTABLISHED = 1;
86     public static final int NOTIFICATION_EVENT_ID_RDS_AVAILABLE = 2;
87     public static final int NOTIFICATION_EVENT_ID_SECURE_SESSION_ABORTED = 3;
88     public static final int NOTIFICATION_EVENT_ID_CONTROLEE_INFO_AVAILABLE = 4;
89 
90     /**
91      * The base class of notification from the FiRa applet.
92      */
93     public static class Notification {
94         @NotificationEventId
95         public final int notificationEventId;
96 
Notification(@otificationEventId int notificationEventId)97         protected Notification(@NotificationEventId int notificationEventId) {
98             this.notificationEventId = notificationEventId;
99         }
100     }
101 
102     /**
103      * The notification of ADF selected.
104      */
105     public static class AdfSelectedNotification extends Notification {
106         @NonNull
107         public final ObjectIdentifier adfOid;
108 
AdfSelectedNotification(@onNull ObjectIdentifier adfOid)109         private AdfSelectedNotification(@NonNull ObjectIdentifier adfOid) {
110             super(NOTIFICATION_EVENT_ID_ADF_SELECTED);
111 
112             this.adfOid = adfOid;
113         }
114     }
115 
116     /**
117      * The notification of the secure channel established.
118      */
119     public static class SecureChannelEstablishedNotification extends Notification {
120         public final Optional<Integer> defaultSessionId;
121 
SecureChannelEstablishedNotification(Optional<Integer> defaultSessionId)122         private SecureChannelEstablishedNotification(Optional<Integer> defaultSessionId) {
123             super(NOTIFICATION_EVENT_ID_SECURE_CHANNEL_ESTABLISHED);
124 
125             this.defaultSessionId = defaultSessionId;
126         }
127     }
128 
129     /**
130      * The notification of the secure session aborted for internal error.
131      */
132     public static class SecureSessionAbortedNotification extends Notification {
SecureSessionAbortedNotification()133         private SecureSessionAbortedNotification() {
134             super(NOTIFICATION_EVENT_ID_SECURE_SESSION_ABORTED);
135         }
136     }
137 
138     /**
139      * The notification of RDS available to be used.
140      */
141     public static class RdsAvailableNotification extends Notification {
142         public final int sessionId;
143 
144         @NonNull
145         public final Optional<byte[]> arbitraryData;
146 
RdsAvailableNotification( int sessionId, @Nullable byte[] arbitraryData)147         private RdsAvailableNotification(
148                 int sessionId, @Nullable byte[] arbitraryData) {
149             super(NOTIFICATION_EVENT_ID_RDS_AVAILABLE);
150             this.sessionId = sessionId;
151             if (arbitraryData == null) {
152                 this.arbitraryData = Optional.empty();
153             } else {
154                 this.arbitraryData = Optional.of(arbitraryData);
155             }
156         }
157     }
158 
159     /**
160      * The notification of the controlee info available.
161      */
162     public static class ControleeInfoAvailableNotification extends Notification {
163         public final byte[] arbitraryData;
164 
ControleeInfoAvailableNotification(@onNull byte[] arbitraryData)165         private ControleeInfoAvailableNotification(@NonNull byte[] arbitraryData) {
166             super(NOTIFICATION_EVENT_ID_CONTROLEE_INFO_AVAILABLE);
167             this.arbitraryData = arbitraryData;
168         }
169     }
170 
171     @TransactionStatus
172     private int mTransactionStatus = TRANSACTION_STATUS_UNDEFINED;
173 
174     /**
175      * The data should be sent to the peer device or host.
176      */
177     @NonNull
178     private Optional<OutboundData> mOutboundData = Optional.empty();
179 
getOutboundData()180     public Optional<OutboundData> getOutboundData() {
181         return mOutboundData;
182     }
183 
184     /**
185      * The notifications got from the Dispatch response.
186      */
187     @NonNull
188     public final List<Notification> notifications;
189 
DispatchResponse(@onNull ResponseApdu responseApdu)190     private DispatchResponse(@NonNull ResponseApdu responseApdu) {
191         super(responseApdu.getStatusWord());
192         notifications = new ArrayList<Notification>();
193         if (!isSuccess()) {
194             return;
195         }
196         Map<Tag, List<TlvDatum>> proprietaryTlvsMap = TlvParser.parseTlvs(responseApdu);
197         List<TlvDatum> proprietaryTlv = proprietaryTlvsMap.get(PROPRIETARY_RESPONSE_TAG);
198         if (proprietaryTlv == null || proprietaryTlv.size() == 0) {
199             logw("no valid dispatch response, root tag is empty.");
200             return;
201         }
202 
203         Map<Tag, List<TlvDatum>> tlvsMap = TlvParser.parseTlvs(proprietaryTlv.get(0).value);
204 
205         notifications.addAll(parseNotification(tlvsMap.get(NOTIFICATION_TAG)));
206 
207         List<TlvDatum> statusTlvs = tlvsMap.get(STATUS_TAG);
208         if (statusTlvs == null || statusTlvs.size() == 0) {
209             logw("no status tag is attached, required by FiRa");
210             return;
211         }
212         mTransactionStatus = parseTransactionStatus(statusTlvs.get(0).value);
213         switch (mTransactionStatus) {
214             case TRANSACTION_STATUS_WITH_ERROR:
215                 notifications.add(new SecureSessionAbortedNotification());
216                 break;
217             case TRANSACTION_STATUS_FORWARD_TO_HOST:
218                 // fall through
219             case TRANSACTION_STATUS_FORWARD_TO_REMOTE:
220                 List<TlvDatum> dataTlvs = tlvsMap.get(DATA_TAG);
221                 if (dataTlvs.size() == 0) {
222                     break;
223                 }
224                 if (mTransactionStatus == TRANSACTION_STATUS_FORWARD_TO_HOST) {
225                     mOutboundData = Optional.of(
226                             new OutboundData(OUTBOUND_TARGET_HOST,
227                                     dataTlvs.get(0).value));
228                 } else {
229                     mOutboundData = Optional.of(
230                             new OutboundData(OUTBOUND_TARGET_REMOTE,
231                                     dataTlvs.get(0).value));
232                 }
233                 break;
234             case TRANSACTION_STATUS_UNDEFINED:
235                 // fall through
236             case TRANSACTION_STATUS_COMPLETE:
237                 // fall through
238             default:
239                 logd("Dispatch response: transaction status: " + mTransactionStatus);
240                 break;
241         }
242     }
243 
244     @TransactionStatus
parseTransactionStatus(@ullable byte[] status)245     private int parseTransactionStatus(@Nullable byte[] status) {
246         if (status == null || status.length < 1) {
247             return TRANSACTION_STATUS_UNDEFINED;
248         }
249         switch (status[0]) {
250             case (byte) 0x00:
251                 return TRANSACTION_STATUS_COMPLETE;
252             case (byte) 0x80:
253                 return TRANSACTION_STATUS_FORWARD_TO_REMOTE;
254             case (byte) 0x81:
255                 return TRANSACTION_STATUS_FORWARD_TO_HOST;
256             case (byte) 0xFF:
257                 return TRANSACTION_STATUS_WITH_ERROR;
258             default:
259                 return TRANSACTION_STATUS_UNDEFINED;
260         }
261     }
262 
263     // throw IllegalStateException
264     @NonNull
parseNotification( @ullable List<TlvDatum> notificationTlvs)265     private List<Notification> parseNotification(
266             @Nullable List<TlvDatum> notificationTlvs) {
267         List<Notification> notificationList = new ArrayList<>();
268         if (notificationTlvs == null || notificationTlvs.size() == 0) {
269             return notificationList;
270         }
271 
272         for (TlvDatum tlv : notificationTlvs) {
273             Map<Tag, List<TlvDatum>> curTlvs = TlvParser.parseTlvs(tlv.value);
274             List<TlvDatum> eventIdTlvs = curTlvs.get(NOTIFICATION_EVENT_ID_TAG);
275             if (eventIdTlvs == null || eventIdTlvs.size() == 0) {
276                 throw new IllegalStateException("Notification event ID is not available.");
277             }
278             byte[] eventIdValue = eventIdTlvs.get(0).value;
279             if (eventIdValue == null || eventIdValue.length == 0) {
280                 throw new IllegalStateException("Notification event ID value is not available.");
281             }
282             switch (eventIdValue[0]) {
283                 case (byte) 0x00:
284                     // parse OID
285                     List<TlvDatum> notificationDataTlvs = curTlvs.get(NOTIFICATION_DATA_TAG);
286                     if (notificationDataTlvs == null || notificationDataTlvs.size() == 0) {
287                         throw new IllegalStateException("Notification data - OID is not available");
288                     }
289 
290                     byte[] adfOidBytes = notificationDataTlvs.get(0).value;
291                     ObjectIdentifier adfOid =
292                             ObjectIdentifier.fromBytes(adfOidBytes);
293 
294                     notificationList.add(new AdfSelectedNotification(adfOid));
295                     break;
296                 case (byte) 0x01:
297                     // TODO: not defined by CSML, may be changed.
298                     Optional<Integer> defaultSessionId = Optional.empty();
299                     notificationDataTlvs = curTlvs.get(NOTIFICATION_DATA_TAG);
300                     if (notificationDataTlvs != null && notificationDataTlvs.size() != 0) {
301                         // try to get the default session Id from the notification.
302                         byte[] payload = notificationDataTlvs.get(0).value;
303                         if (payload == null || payload.length < 2
304                                 || payload.length < 1 + payload[0]) {
305                             logd("not valid session id in sc established notification.");
306                         } else {
307                             int sessionIdLen = payload[0];
308                             byte[] sessionId = new byte[sessionIdLen];
309                             System.arraycopy(payload, 1, sessionId, 0, sessionIdLen);
310                             defaultSessionId = Optional.of(
311                                     DataTypeConversionUtil.arbitraryByteArrayToI32(sessionId));
312                         }
313                     }
314                     notificationList.add(
315                             new SecureChannelEstablishedNotification(defaultSessionId));
316                     break;
317                 case (byte) 0x02:
318                     // parse sessionId and arbitrary data
319                     notificationDataTlvs = curTlvs.get(NOTIFICATION_DATA_TAG);
320                     if (notificationDataTlvs == null || notificationDataTlvs.size() == 0) {
321                         throw new IllegalStateException(
322                                 "RDS Notification data - sessionId is not available");
323                     }
324                     byte[] payload = notificationDataTlvs.get(0).value;
325                     if (payload == null || payload.length < 2 || payload.length < 1 + payload[0]) {
326                         throw new IllegalStateException(
327                                 "RDS Notification data - bad payload");
328                     }
329                     int sessionIdLen = payload[0];
330                     byte[] sessionId = new byte[sessionIdLen];
331                     System.arraycopy(payload, 1, sessionId, 0, sessionIdLen);
332 
333                     byte[] arbitraryData = new byte[0];
334                     int arbitraryDataOffset = sessionIdLen + 1;
335                     if (payload.length > arbitraryDataOffset) {
336                         int arbitraryDataLen = payload[arbitraryDataOffset];
337                         if (payload.length == 2 + sessionIdLen + arbitraryDataLen) {
338                             arbitraryData = new byte[arbitraryDataLen];
339                             System.arraycopy(payload, arbitraryDataOffset + 1,
340                                     arbitraryData, 0, arbitraryDataLen);
341                         }
342                     }
343 
344                     notificationList.add(
345                             new RdsAvailableNotification(
346                                     DataTypeConversionUtil.arbitraryByteArrayToI32(sessionId),
347                                     arbitraryData));
348                     break;
349                 case (byte) 0x03:
350                     // TODO: change it according to the final CSML spec, this is not defined yet.
351                     // use 0x03 and controlee info data as notification data.
352                     notificationDataTlvs = curTlvs.get(NOTIFICATION_DATA_TAG);
353                     arbitraryData = new byte[0];
354                     if (notificationDataTlvs != null && notificationDataTlvs.size() != 0) {
355                         payload = notificationDataTlvs.get(0).value;
356                         if (payload == null || payload.length == 0) {
357                             throw new IllegalStateException(
358                                     "payload of controlee info available notification is bad.");
359                         }
360                         arbitraryData = new byte[payload.length];
361                         System.arraycopy(payload, 0, arbitraryData, 0, payload.length);
362                     }
363                     notificationList.add(new ControleeInfoAvailableNotification(arbitraryData));
364                     break;
365                 default:
366             }
367         }
368 
369         return notificationList;
370     }
371 
372     /**
373      * Parse the response of DispatchCommand.
374      */
375     @NonNull
fromResponseApdu(@onNull ResponseApdu responseApdu)376     public static DispatchResponse fromResponseApdu(@NonNull ResponseApdu responseApdu) {
377         return new DispatchResponse(responseApdu);
378     }
379 
380     @IntDef(prefix = { "OUTBOUND_TARGET_" }, value = {
381             OUTBOUND_TARGET_HOST,
382             OUTBOUND_TARGET_REMOTE,
383     })
384     @Retention(RetentionPolicy.SOURCE)
385     public @interface OutboundTarget {}
386 
387     public static final int OUTBOUND_TARGET_HOST = 0;
388     public static final int OUTBOUND_TARGET_REMOTE = 1;
389 
390     /**
391      * The outbound data from the DispatchResponse.
392      */
393     public static class OutboundData {
394         @OutboundTarget
395         public final int target;
396         public final byte[] data;
397 
OutboundData(@utboundTarget int target, byte[] data)398         private OutboundData(@OutboundTarget int target, byte[] data) {
399             this.target = target;
400             this.data = data;
401         }
402     }
403 
logw(@onNull String dbgMsg)404     private void logw(@NonNull String dbgMsg) {
405         Log.w(LOG_TAG, dbgMsg);
406     }
logd(@onNull String dbgMsg)407     private void logd(@NonNull String dbgMsg) {
408         Log.d(LOG_TAG, dbgMsg);
409     }
410 }
411