1 /*
2  * Copyright (C) 2024 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 
17 package com.android.server.wearable;
18 
19 import android.annotation.NonNull;
20 import android.companion.AssociationInfo;
21 import android.companion.AssociationRequest;
22 import android.companion.CompanionDeviceManager;
23 import android.os.Binder;
24 import android.os.ParcelFileDescriptor;
25 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
26 import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
27 import android.util.Slog;
28 
29 import com.android.internal.annotations.GuardedBy;
30 
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.OutputStream;
34 import java.util.List;
35 import java.util.Objects;
36 import java.util.concurrent.Executor;
37 import java.util.concurrent.ExecutorService;
38 import java.util.concurrent.Executors;
39 import java.util.concurrent.RejectedExecutionException;
40 import java.util.concurrent.atomic.AtomicBoolean;
41 import java.util.function.BiConsumer;
42 import java.util.function.Consumer;
43 
44 /**
45  * A wrapper that manages a CompanionDeviceManager secure channel for wearable sensing.
46  *
47  * <p>This wrapper accepts a connection to a wearable from the caller. It then attaches the
48  * connection to the CompanionDeviceManager via {@link
49  * CompanionDeviceManager#attachSystemDataTransport(int, InputStream, OutputStream)}, which will
50  * create an encrypted channel using the provided connection as the raw underlying connection. The
51  * wearable device is expected to attach its side of the raw connection to its
52  * CompanionDeviceManager via the same method so that the two CompanionDeviceManagers on the two
53  * devices can perform attestation and set up the encrypted channel. Attestation requirements are
54  * listed in {@link com.android.server.security.AttestationVerificationPeerDeviceVerifier}.
55  *
56  * <p>When the encrypted channel is available, it will be provided to the caller via the
57  * SecureTransportListener.
58  */
59 final class WearableSensingSecureChannel {
60 
61     /** A listener for secure transport and its error signal. */
62     interface SecureTransportListener {
63 
64         /** Called when the secure transport is available. */
onSecureTransportAvailable(ParcelFileDescriptor secureTransport)65         void onSecureTransportAvailable(ParcelFileDescriptor secureTransport);
66 
67         /**
68          * Called when there is a non-recoverable error. The secure channel will be automatically
69          * closed.
70          */
onError()71         void onError();
72     }
73 
74     private static final String TAG = WearableSensingSecureChannel.class.getSimpleName();
75     private static final String CDM_ASSOCIATION_DISPLAY_NAME = "PlaceholderDisplayNameFromWSM";
76     // The batch size of reading from the ParcelFileDescriptor returned to mSecureTransportListener
77     private static final int READ_BUFFER_SIZE = 8192;
78 
79     private final Object mLock = new Object();
80     // CompanionDeviceManager (CDM) can continue to call these ExecutorServices even after the
81     // corresponding cleanup methods in CDM have been called (e.g.
82     // removeOnTransportsChangedListener). Since we shut down these ExecutorServices after
83     // clean up, we use SoftShutdownExecutor to suppress RejectedExecutionExceptions.
84     private final SoftShutdownExecutor mMessageFromWearableExecutor =
85             new SoftShutdownExecutor(Executors.newSingleThreadExecutor());
86     private final SoftShutdownExecutor mMessageToWearableExecutor =
87             new SoftShutdownExecutor(Executors.newSingleThreadExecutor());
88     private final SoftShutdownExecutor mLightWeightExecutor =
89             new SoftShutdownExecutor(Executors.newSingleThreadExecutor());
90     private final CompanionDeviceManager mCompanionDeviceManager;
91     private final ParcelFileDescriptor mUnderlyingTransport;
92     private final SecureTransportListener mSecureTransportListener;
93     private final AtomicBoolean mTransportAvailable = new AtomicBoolean(false);
94     private final Consumer<List<AssociationInfo>> mOnTransportsChangedListener =
95             this::onTransportsChanged;
96     private final BiConsumer<Integer, byte[]> mOnMessageReceivedListener = this::onMessageReceived;
97     private final ParcelFileDescriptor mRemoteFd; // To be returned to mSecureTransportListener
98     // read input received from the ParcelFileDescriptor returned to mSecureTransportListener
99     private final InputStream mLocalIn;
100     // send output to the ParcelFileDescriptor returned to mSecureTransportListener
101     private final OutputStream mLocalOut;
102 
103     @GuardedBy("mLock")
104     private boolean mClosed = false;
105 
106     private Integer mAssociationId = null;
107 
108     /**
109      * Creates a WearableSensingSecureChannel. When the secure transport is ready,
110      * secureTransportListener will be notified.
111      *
112      * @param companionDeviceManager The CompanionDeviceManager system service.
113      * @param underlyingTransport The underlying transport to create the secure channel on.
114      * @param secureTransportListener The listener to receive the secure transport when it is ready.
115      * @throws IOException if it cannot create a {@link ParcelFileDescriptor} socket pair.
116      */
create( @onNull CompanionDeviceManager companionDeviceManager, @NonNull ParcelFileDescriptor underlyingTransport, @NonNull SecureTransportListener secureTransportListener)117     static WearableSensingSecureChannel create(
118             @NonNull CompanionDeviceManager companionDeviceManager,
119             @NonNull ParcelFileDescriptor underlyingTransport,
120             @NonNull SecureTransportListener secureTransportListener)
121             throws IOException {
122         Objects.requireNonNull(companionDeviceManager);
123         Objects.requireNonNull(underlyingTransport);
124         Objects.requireNonNull(secureTransportListener);
125         ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair();
126         WearableSensingSecureChannel channel =
127                 new WearableSensingSecureChannel(
128                         companionDeviceManager,
129                         underlyingTransport,
130                         secureTransportListener,
131                         pair[0],
132                         pair[1]);
133         channel.initialize();
134         return channel;
135     }
136 
WearableSensingSecureChannel( CompanionDeviceManager companionDeviceManager, ParcelFileDescriptor underlyingTransport, SecureTransportListener secureTransportListener, ParcelFileDescriptor remoteFd, ParcelFileDescriptor localFd)137     private WearableSensingSecureChannel(
138             CompanionDeviceManager companionDeviceManager,
139             ParcelFileDescriptor underlyingTransport,
140             SecureTransportListener secureTransportListener,
141             ParcelFileDescriptor remoteFd,
142             ParcelFileDescriptor localFd) {
143         mCompanionDeviceManager = companionDeviceManager;
144         mUnderlyingTransport = underlyingTransport;
145         mSecureTransportListener = secureTransportListener;
146         mRemoteFd = remoteFd;
147         mLocalIn = new AutoCloseInputStream(localFd);
148         mLocalOut = new AutoCloseOutputStream(localFd);
149     }
150 
initialize()151     private void initialize() {
152         final long originalCallingIdentity = Binder.clearCallingIdentity();
153         try {
154             Slog.d(TAG, "Requesting CDM association.");
155             mCompanionDeviceManager.associate(
156                     new AssociationRequest.Builder()
157                             .setDisplayName(CDM_ASSOCIATION_DISPLAY_NAME)
158                             .setSelfManaged(true)
159                             .build(),
160                     mLightWeightExecutor,
161                     new CompanionDeviceManager.Callback() {
162                         @Override
163                         public void onAssociationCreated(AssociationInfo associationInfo) {
164                             WearableSensingSecureChannel.this.onAssociationCreated(
165                                     associationInfo.getId());
166                         }
167 
168                         @Override
169                         public void onFailure(CharSequence error) {
170                             Slog.e(
171                                     TAG,
172                                     "Failed to create CompanionDeviceManager association: "
173                                             + error);
174                             onError();
175                         }
176                     });
177         } finally {
178             Binder.restoreCallingIdentity(originalCallingIdentity);
179         }
180     }
181 
onAssociationCreated(int associationId)182     private void onAssociationCreated(int associationId) {
183         Slog.i(TAG, "CDM association created.");
184         synchronized (mLock) {
185             if (mClosed) {
186                 return;
187             }
188             mAssociationId = associationId;
189             mCompanionDeviceManager.addOnMessageReceivedListener(
190                     mMessageFromWearableExecutor,
191                     CompanionDeviceManager.MESSAGE_ONEWAY_FROM_WEARABLE,
192                     mOnMessageReceivedListener);
193             mCompanionDeviceManager.addOnTransportsChangedListener(
194                     mLightWeightExecutor, mOnTransportsChangedListener);
195             mCompanionDeviceManager.attachSystemDataTransport(
196                     associationId,
197                     new AutoCloseInputStream(mUnderlyingTransport),
198                     new AutoCloseOutputStream(mUnderlyingTransport));
199         }
200     }
201 
onTransportsChanged(List<AssociationInfo> associationInfos)202     private void onTransportsChanged(List<AssociationInfo> associationInfos) {
203         synchronized (mLock) {
204             if (mClosed) {
205                 return;
206             }
207             if (mAssociationId == null) {
208                 Slog.e(TAG, "mAssociationId is null when transport changed");
209                 return;
210             }
211         }
212         // Do not call onTransportAvailable() or onError() when holding the lock because it can
213         // cause a deadlock if the callback holds another lock.
214         boolean transportAvailable =
215                 associationInfos.stream().anyMatch(info -> info.getId() == mAssociationId);
216         if (transportAvailable && mTransportAvailable.compareAndSet(false, true)) {
217             onTransportAvailable();
218         } else if (!transportAvailable && mTransportAvailable.compareAndSet(true, false)) {
219             Slog.i(TAG, "CDM transport is detached. This is not recoverable.");
220             onError();
221         }
222     }
223 
onTransportAvailable()224     private void onTransportAvailable() {
225         // Start sending data received from the remote stream to the wearable.
226         Slog.i(TAG, "Transport available");
227         mMessageToWearableExecutor.execute(
228                 () -> {
229                     int[] associationIdsToSendMessageTo = new int[] {mAssociationId};
230                     byte[] buffer = new byte[READ_BUFFER_SIZE];
231                     int readLen;
232                     try {
233                         while ((readLen = mLocalIn.read(buffer)) != -1) {
234                             byte[] data = new byte[readLen];
235                             System.arraycopy(buffer, 0, data, 0, readLen);
236                             Slog.v(TAG, "Sending message to wearable");
237                             mCompanionDeviceManager.sendMessage(
238                                     CompanionDeviceManager.MESSAGE_ONEWAY_TO_WEARABLE,
239                                     data,
240                                     associationIdsToSendMessageTo);
241                         }
242                     } catch (IOException e) {
243                         Slog.i(TAG, "IOException while reading from remote stream.");
244                         onError();
245                         return;
246                     }
247                     Slog.i(
248                             TAG,
249                             "Reached EOF when reading from remote stream. Reporting this as an"
250                                     + " error.");
251                     onError();
252                 });
253         mSecureTransportListener.onSecureTransportAvailable(mRemoteFd);
254     }
255 
onMessageReceived(int associationIdForMessage, byte[] data)256     private void onMessageReceived(int associationIdForMessage, byte[] data) {
257         if (associationIdForMessage == mAssociationId) {
258             Slog.v(TAG, "Received message from wearable.");
259             try {
260                 mLocalOut.write(data);
261                 mLocalOut.flush();
262             } catch (IOException e) {
263                 Slog.i(
264                         TAG,
265                         "IOException when writing to remote stream. Closing the secure channel.");
266                 onError();
267             }
268         } else {
269             Slog.v(
270                     TAG,
271                     "Received CDM message of type MESSAGE_ONEWAY_FROM_WEARABLE, but it is for"
272                         + " another association. Ignoring the message.");
273         }
274     }
275 
onError()276     private void onError() {
277         synchronized (mLock) {
278             if (mClosed) {
279                 return;
280             }
281         }
282         mSecureTransportListener.onError();
283         close();
284     }
285 
286     /** Closes this secure channel and releases all resources. */
close()287     void close() {
288         synchronized (mLock) {
289             if (mClosed) {
290                 return;
291             }
292             Slog.i(TAG, "Closing WearableSensingSecureChannel.");
293             mClosed = true;
294             if (mAssociationId != null) {
295                 final long originalCallingIdentity = Binder.clearCallingIdentity();
296                 try {
297                     mCompanionDeviceManager.removeOnTransportsChangedListener(
298                             mOnTransportsChangedListener);
299                     mCompanionDeviceManager.removeOnMessageReceivedListener(
300                             CompanionDeviceManager.MESSAGE_ONEWAY_FROM_WEARABLE,
301                             mOnMessageReceivedListener);
302                     mCompanionDeviceManager.detachSystemDataTransport(mAssociationId);
303                     mCompanionDeviceManager.disassociate(mAssociationId);
304                 } finally {
305                     Binder.restoreCallingIdentity(originalCallingIdentity);
306                 }
307             }
308             try {
309                 mLocalIn.close();
310             } catch (IOException ex) {
311                 Slog.e(TAG, "Encountered IOException when closing local input stream.", ex);
312             }
313             try {
314                 mLocalOut.close();
315             } catch (IOException ex) {
316                 Slog.e(TAG, "Encountered IOException when closing local output stream.", ex);
317             }
318             mMessageFromWearableExecutor.shutdown();
319             mMessageToWearableExecutor.shutdown();
320             mLightWeightExecutor.shutdown();
321         }
322     }
323 
324     /**
325      * An executor that can be shutdown. Unlike an ExecutorService, it will not throw a
326      * RejectedExecutionException if {@link #execute(Runnable)} is called after shutdown.
327      */
328     private static class SoftShutdownExecutor implements Executor {
329 
330         private final ExecutorService mExecutorService;
331 
SoftShutdownExecutor(ExecutorService executorService)332         SoftShutdownExecutor(ExecutorService executorService) {
333             mExecutorService = executorService;
334         }
335 
336         @Override
execute(Runnable runnable)337         public void execute(Runnable runnable) {
338             try {
339                 mExecutorService.execute(runnable);
340             } catch (RejectedExecutionException ex) {
341                 Slog.d(TAG, "Received new runnable after shutdown. Ignoring.");
342             }
343         }
344 
345         /** Shutdown the underlying ExecutorService. */
shutdown()346         void shutdown() {
347             mExecutorService.shutdown();
348         }
349     }
350 }
351