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 import com.android.internal.util.Preconditions;
37 
38 import java.security.SecureRandom;
39 import java.util.Map;
40 import java.util.Set;
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         Preconditions.checkStringNotEmpty(callingPackageName,
120                 "callingPackageName cannot be null or empty.");
121         Preconditions.checkNotNull(intent, "intent cannot be null");
122         // Check calling uid matches callingpkg.
123         AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
124         appOps.checkPackage(Binder.getCallingUid(), callingPackageName);
125 
126         // Generate a token to store the request under.
127         String token = PackageBasedTokenUtil.generateToken(mContext, callingPackageName);
128         if (token != null) {
129             synchronized (mLock) {
130                 // Only allow one request in flight from a package.
131                 if (mPackageMap.containsKey(callingPackageName)) {
132                     removeRequestLocked(mPackageMap.get(callingPackageName));
133                 }
134                 // Store state.
135                 AppRequestInfo info = new AppRequestInfo(
136                         callingPackageName, intent, token, prefixes, subId, true);
137                 addRequestLocked(info);
138             }
139         }
140         return token;
141     }
142 
143     /**
144      * Handle an incoming SMS_DELIVER_ACTION intent if it is an app-only SMS.
145      */
handleSmsReceivedIntent(Intent intent)146     public boolean handleSmsReceivedIntent(Intent intent) {
147         // Correctness check the action.
148         if (intent.getAction() != Intents.SMS_DELIVER_ACTION) {
149             Log.wtf(LOG_TAG, "Got intent with incorrect action: " + intent.getAction());
150             return false;
151         }
152 
153         synchronized (mLock) {
154             removeExpiredTokenLocked();
155 
156             String message = extractMessage(intent);
157             if (TextUtils.isEmpty(message)) {
158                 return false;
159             }
160 
161             AppRequestInfo info = findAppRequestInfoSmsIntentLocked(message);
162             if (info == null) {
163                 // The message didn't contain a token -- nothing to do.
164                 return false;
165             }
166 
167             try {
168                 Intent fillIn = new Intent()
169                         .putExtras(intent.getExtras())
170                         .putExtra(SmsManager.EXTRA_STATUS, SmsManager.RESULT_STATUS_SUCCESS)
171                         .putExtra(SmsManager.EXTRA_SMS_MESSAGE, message)
172                         .putExtra(SmsManager.EXTRA_SIM_SUBSCRIPTION_ID, info.subId)
173                         .addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
174 
175                 info.pendingIntent.send(mContext, 0, fillIn);
176             } catch (PendingIntent.CanceledException e) {
177                 // The pending intent is canceled, send this SMS as normal.
178                 removeRequestLocked(info);
179                 return false;
180             }
181 
182             removeRequestLocked(info);
183             return true;
184         }
185     }
186 
removeExpiredTokenLocked()187     private void removeExpiredTokenLocked() {
188         final long currentTimeMillis = System.currentTimeMillis();
189 
190         final Set<String> keySet = mTokenMap.keySet();
191         for (String token : keySet) {
192             AppRequestInfo request = mTokenMap.get(token);
193             if (request.packageBasedToken
194                     && (currentTimeMillis - TIMEOUT_MILLIS > request.timestamp)) {
195                 // Send the provided intent with SMS retriever status
196                 try {
197                     Intent fillIn = new Intent()
198                             .putExtra(SmsManager.EXTRA_STATUS,
199                                     SmsManager.RESULT_STATUS_TIMEOUT)
200                             .addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
201                     request.pendingIntent.send(mContext, 0, fillIn);
202                 } catch (PendingIntent.CanceledException e) {
203                     // do nothing
204                 }
205 
206                 removeRequestLocked(request);
207             }
208         }
209     }
210 
extractMessage(Intent intent)211     private String extractMessage(Intent intent) {
212         SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
213         if (messages == null) {
214             return null;
215         }
216         StringBuilder fullMessageBuilder = new StringBuilder();
217         for (SmsMessage message : messages) {
218             if (message == null || message.getMessageBody() == null) {
219                 continue;
220             }
221             fullMessageBuilder.append(message.getMessageBody());
222         }
223 
224         return fullMessageBuilder.toString();
225     }
226 
findAppRequestInfoSmsIntentLocked(String fullMessage)227     private AppRequestInfo findAppRequestInfoSmsIntentLocked(String fullMessage) {
228         // Look for any tokens in the full message.
229         for (String token : mTokenMap.keySet()) {
230             if (fullMessage.trim().contains(token) && hasPrefix(token, fullMessage)) {
231                 return mTokenMap.get(token);
232             }
233         }
234         return null;
235     }
236 
generateNonce()237     private String generateNonce() {
238         byte[] bytes = new byte[8];
239         mRandom.nextBytes(bytes);
240         return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
241     }
242 
hasPrefix(String token, String message)243     private boolean hasPrefix(String token, String message) {
244         AppRequestInfo request = mTokenMap.get(token);
245         if (TextUtils.isEmpty(request.prefixes)) {
246             return true;
247         }
248 
249         String[] prefixes = request.prefixes.split(SmsManager.REGEX_PREFIX_DELIMITER);
250         for (String prefix : prefixes) {
251             if (message.startsWith(prefix)) {
252                 return true;
253             }
254         }
255         return false;
256     }
257 
removeRequestLocked(AppRequestInfo info)258     private void removeRequestLocked(AppRequestInfo info) {
259         mTokenMap.remove(info.token);
260         mPackageMap.remove(info.packageName);
261     }
262 
addRequestLocked(AppRequestInfo info)263     private void addRequestLocked(AppRequestInfo info) {
264         mTokenMap.put(info.token, info);
265         mPackageMap.put(info.packageName, info);
266     }
267 
268     private final class AppRequestInfo {
269         public final String packageName;
270         public final PendingIntent pendingIntent;
271         public final String token;
272         public final long timestamp;
273         public final String prefixes;
274         public final int subId;
275         public final boolean packageBasedToken;
276 
AppRequestInfo(String packageName, PendingIntent pendingIntent, String token)277         AppRequestInfo(String packageName, PendingIntent pendingIntent, String token) {
278           this(packageName, pendingIntent, token, null,
279                   SubscriptionManager.INVALID_SUBSCRIPTION_ID, false);
280         }
281 
AppRequestInfo(String packageName, PendingIntent pendingIntent, String token, String prefixes, int subId, boolean packageBasedToken)282         AppRequestInfo(String packageName, PendingIntent pendingIntent, String token,
283                 String prefixes, int subId, boolean packageBasedToken) {
284             this.packageName = packageName;
285             this.pendingIntent = pendingIntent;
286             this.token = token;
287             this.timestamp = System.currentTimeMillis();
288             this.prefixes = prefixes;
289             this.subId = subId;
290             this.packageBasedToken = packageBasedToken;
291         }
292     }
293 
294 }
295