1 /*
2  * Copyright (C) 2016 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.internal.telephony;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.AppOpsManager;
22 import android.app.PendingIntent;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.os.Binder;
26 import android.provider.Telephony.Sms.Intents;
27 import android.telephony.SmsManager;
28 import android.telephony.SmsMessage;
29 import android.telephony.SubscriptionManager;
30 import android.text.TextUtils;
31 import android.util.ArrayMap;
32 import android.util.Base64;
33 import android.util.Log;
34 
35 import com.android.internal.annotations.GuardedBy;
36 
37 import java.security.SecureRandom;
38 import java.util.Iterator;
39 import java.util.Map;
40 import java.util.Objects;
41 import java.util.concurrent.TimeUnit;
42 
43 
44 /**
45  *  Manager for app specific SMS requests. This can be used to implement SMS based
46  *  communication channels (e.g. for SMS based phone number verification) without needing the
47  *  {@link Manifest.permission#RECEIVE_SMS} permission.
48  *
49  *  {@link #createAppSpecificSmsRequest} allows an application to provide a {@link PendingIntent}
50  *  that is triggered when an incoming SMS is received that contains the provided token.
51  */
52 public class AppSmsManager {
53     private static final String LOG_TAG = "AppSmsManager";
54 
55     private static final long TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);
56     private final SecureRandom mRandom;
57     private final Context mContext;
58     private final Object mLock = new Object();
59 
60     @GuardedBy("mLock")
61     private final Map<String, AppRequestInfo> mTokenMap;
62     @GuardedBy("mLock")
63     private final Map<String, AppRequestInfo> mPackageMap;
64 
AppSmsManager(Context context)65     public AppSmsManager(Context context) {
66         mRandom = new SecureRandom();
67         mTokenMap = new ArrayMap<>();
68         mPackageMap = new ArrayMap<>();
69         mContext = context;
70     }
71 
72     /**
73      * Create an app specific incoming SMS request for the the calling package.
74      *
75      * This method returns a token that if included in a subsequent incoming SMS message the
76      * {@link Intents.SMS_RECEIVED_ACTION} intent will be delivered only to the calling package and
77      * will not require the application have the {@link Manifest.permission#RECEIVE_SMS} permission.
78      *
79      * An app can only have one request at a time, if the app already has a request it will be
80      * dropped and the new one will be added.
81      *
82      * @return Token to include in an SMS to have it delivered directly to the app.
83      */
createAppSpecificSmsToken(String callingPkg, PendingIntent intent)84     public String createAppSpecificSmsToken(String callingPkg, PendingIntent intent) {
85         // Check calling uid matches callingpkg.
86         AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
87         appOps.checkPackage(Binder.getCallingUid(), callingPkg);
88 
89         // Generate a nonce to store the request under.
90         String token = generateNonce();
91         synchronized (mLock) {
92             // Only allow one request in flight from a package.
93             if (mPackageMap.containsKey(callingPkg)) {
94                 removeRequestLocked(mPackageMap.get(callingPkg));
95             }
96             // Store state.
97             AppRequestInfo info = new AppRequestInfo(callingPkg, intent, token);
98             addRequestLocked(info);
99         }
100         return token;
101     }
102 
103     /**
104      * Create an app specific incoming SMS request for the the calling package.
105      *
106      * This method returns a token that if included in a subsequent incoming SMS message the
107      * {@link Intents.SMS_RECEIVED_ACTION} intent will be delivered only to the calling package and
108      * will not require the application have the {@link Manifest.permission#RECEIVE_SMS} permission.
109      *
110      * An app can only have one request at a time, if the app already has a request it will be
111      * dropped and the new one will be added.
112      *
113      * @return Token to include in an SMS to have it delivered directly to the app.
114      */
createAppSpecificSmsTokenWithPackageInfo(int subId, @NonNull String callingPackageName, @Nullable String prefixes, @NonNull PendingIntent intent)115     public String createAppSpecificSmsTokenWithPackageInfo(int subId,
116             @NonNull String callingPackageName,
117             @Nullable String prefixes,
118             @NonNull PendingIntent intent) {
119         if (TextUtils.isEmpty(callingPackageName)) {
120             throw new IllegalArgumentException("callingPackageName cannot be null or empty.");
121         }
122         Objects.requireNonNull(intent, "intent cannot be null");
123         // Check calling uid matches callingpkg.
124         AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
125         appOps.checkPackage(Binder.getCallingUid(), callingPackageName);
126 
127         // Generate a token to store the request under.
128         String token = PackageBasedTokenUtil.generateToken(mContext, callingPackageName);
129         if (token != null) {
130             synchronized (mLock) {
131                 // Only allow one request in flight from a package.
132                 if (mPackageMap.containsKey(callingPackageName)) {
133                     removeRequestLocked(mPackageMap.get(callingPackageName));
134                 }
135                 // Store state.
136                 AppRequestInfo info = new AppRequestInfo(
137                         callingPackageName, intent, token, prefixes, subId, true);
138                 addRequestLocked(info);
139             }
140         }
141         return token;
142     }
143 
144     /**
145      * Handle an incoming SMS_DELIVER_ACTION intent if it is an app-only SMS.
146      */
handleSmsReceivedIntent(Intent intent)147     public boolean handleSmsReceivedIntent(Intent intent) {
148         // Correctness check the action.
149         if (intent.getAction() != Intents.SMS_DELIVER_ACTION) {
150             Log.wtf(LOG_TAG, "Got intent with incorrect action: " + intent.getAction());
151             return false;
152         }
153 
154         synchronized (mLock) {
155             removeExpiredTokenLocked();
156 
157             String message = extractMessage(intent);
158             if (TextUtils.isEmpty(message)) {
159                 return false;
160             }
161 
162             AppRequestInfo info = findAppRequestInfoSmsIntentLocked(message);
163             if (info == null) {
164                 // The message didn't contain a token -- nothing to do.
165                 return false;
166             }
167 
168             try {
169                 Intent fillIn = new Intent()
170                         .putExtras(intent.getExtras())
171                         .putExtra(SmsManager.EXTRA_STATUS, SmsManager.RESULT_STATUS_SUCCESS)
172                         .putExtra(SmsManager.EXTRA_SMS_MESSAGE, message)
173                         .putExtra(SmsManager.EXTRA_SIM_SUBSCRIPTION_ID, info.subId)
174                         .addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
175 
176                 info.pendingIntent.send(mContext, 0, fillIn);
177             } catch (PendingIntent.CanceledException e) {
178                 // The pending intent is canceled, send this SMS as normal.
179                 removeRequestLocked(info);
180                 return false;
181             }
182 
183             removeRequestLocked(info);
184             return true;
185         }
186     }
187 
removeExpiredTokenLocked()188     private void removeExpiredTokenLocked() {
189         final long currentTimeMillis = System.currentTimeMillis();
190 
191         Iterator<Map.Entry<String, AppRequestInfo>> iterator = mTokenMap.entrySet().iterator();
192         while (iterator.hasNext()) {
193             Map.Entry<String, AppRequestInfo> entry = iterator.next();
194             AppRequestInfo request = entry.getValue();
195             if (request.packageBasedToken
196                     && (currentTimeMillis - TIMEOUT_MILLIS > request.timestamp)) {
197                 // Send the provided intent with SMS retriever status
198                 try {
199                     Intent fillIn = new Intent()
200                             .putExtra(SmsManager.EXTRA_STATUS,
201                                     SmsManager.RESULT_STATUS_TIMEOUT)
202                             .addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
203                     request.pendingIntent.send(mContext, 0, fillIn);
204                 } catch (PendingIntent.CanceledException e) {
205                     // do nothing
206                 }
207                 // Remove from mTokenMap and mPackageMap
208                 mPackageMap.remove(entry.getValue().packageName);
209                 iterator.remove();
210             }
211         }
212     }
213 
extractMessage(Intent intent)214     private String extractMessage(Intent intent) {
215         SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
216         if (messages == null) {
217             return null;
218         }
219         StringBuilder fullMessageBuilder = new StringBuilder();
220         for (SmsMessage message : messages) {
221             if (message == null || message.getMessageBody() == null) {
222                 continue;
223             }
224             fullMessageBuilder.append(message.getMessageBody());
225         }
226 
227         return fullMessageBuilder.toString();
228     }
229 
findAppRequestInfoSmsIntentLocked(String fullMessage)230     private AppRequestInfo findAppRequestInfoSmsIntentLocked(String fullMessage) {
231         // Look for any tokens in the full message.
232         for (String token : mTokenMap.keySet()) {
233             if (fullMessage.trim().contains(token) && hasPrefix(token, fullMessage)) {
234                 return mTokenMap.get(token);
235             }
236         }
237         return null;
238     }
239 
generateNonce()240     private String generateNonce() {
241         byte[] bytes = new byte[8];
242         mRandom.nextBytes(bytes);
243         return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
244     }
245 
hasPrefix(String token, String message)246     private boolean hasPrefix(String token, String message) {
247         AppRequestInfo request = mTokenMap.get(token);
248         if (TextUtils.isEmpty(request.prefixes)) {
249             return true;
250         }
251 
252         String[] prefixes = request.prefixes.split(SmsManager.REGEX_PREFIX_DELIMITER);
253         for (String prefix : prefixes) {
254             if (message.startsWith(prefix)) {
255                 return true;
256             }
257         }
258         return false;
259     }
260 
removeRequestLocked(AppRequestInfo info)261     private void removeRequestLocked(AppRequestInfo info) {
262         mTokenMap.remove(info.token);
263         mPackageMap.remove(info.packageName);
264     }
265 
addRequestLocked(AppRequestInfo info)266     private void addRequestLocked(AppRequestInfo info) {
267         mTokenMap.put(info.token, info);
268         mPackageMap.put(info.packageName, info);
269     }
270 
271     private final class AppRequestInfo {
272         public final String packageName;
273         public final PendingIntent pendingIntent;
274         public final String token;
275         public final long timestamp;
276         public final String prefixes;
277         public final int subId;
278         public final boolean packageBasedToken;
279 
AppRequestInfo(String packageName, PendingIntent pendingIntent, String token)280         AppRequestInfo(String packageName, PendingIntent pendingIntent, String token) {
281           this(packageName, pendingIntent, token, null,
282                   SubscriptionManager.INVALID_SUBSCRIPTION_ID, false);
283         }
284 
AppRequestInfo(String packageName, PendingIntent pendingIntent, String token, String prefixes, int subId, boolean packageBasedToken)285         AppRequestInfo(String packageName, PendingIntent pendingIntent, String token,
286                 String prefixes, int subId, boolean packageBasedToken) {
287             this.packageName = packageName;
288             this.pendingIntent = pendingIntent;
289             this.token = token;
290             this.timestamp = System.currentTimeMillis();
291             this.prefixes = prefixes;
292             this.subId = subId;
293             this.packageBasedToken = packageBasedToken;
294         }
295     }
296 
297 }
298