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.Iterator;
40 import java.util.Map;
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         Iterator<Map.Entry<String, AppRequestInfo>> iterator = mTokenMap.entrySet().iterator();
191         while (iterator.hasNext()) {
192             Map.Entry<String, AppRequestInfo> entry = iterator.next();
193             AppRequestInfo request = entry.getValue();
194             if (request.packageBasedToken
195                     && (currentTimeMillis - TIMEOUT_MILLIS > request.timestamp)) {
196                 // Send the provided intent with SMS retriever status
197                 try {
198                     Intent fillIn = new Intent()
199                             .putExtra(SmsManager.EXTRA_STATUS,
200                                     SmsManager.RESULT_STATUS_TIMEOUT)
201                             .addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
202                     request.pendingIntent.send(mContext, 0, fillIn);
203                 } catch (PendingIntent.CanceledException e) {
204                     // do nothing
205                 }
206                 // Remove from mTokenMap and mPackageMap
207                 iterator.remove();
208                 mPackageMap.remove(entry.getValue().packageName);
209             }
210         }
211     }
212 
extractMessage(Intent intent)213     private String extractMessage(Intent intent) {
214         SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
215         if (messages == null) {
216             return null;
217         }
218         StringBuilder fullMessageBuilder = new StringBuilder();
219         for (SmsMessage message : messages) {
220             if (message == null || message.getMessageBody() == null) {
221                 continue;
222             }
223             fullMessageBuilder.append(message.getMessageBody());
224         }
225 
226         return fullMessageBuilder.toString();
227     }
228 
findAppRequestInfoSmsIntentLocked(String fullMessage)229     private AppRequestInfo findAppRequestInfoSmsIntentLocked(String fullMessage) {
230         // Look for any tokens in the full message.
231         for (String token : mTokenMap.keySet()) {
232             if (fullMessage.trim().contains(token) && hasPrefix(token, fullMessage)) {
233                 return mTokenMap.get(token);
234             }
235         }
236         return null;
237     }
238 
generateNonce()239     private String generateNonce() {
240         byte[] bytes = new byte[8];
241         mRandom.nextBytes(bytes);
242         return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
243     }
244 
hasPrefix(String token, String message)245     private boolean hasPrefix(String token, String message) {
246         AppRequestInfo request = mTokenMap.get(token);
247         if (TextUtils.isEmpty(request.prefixes)) {
248             return true;
249         }
250 
251         String[] prefixes = request.prefixes.split(SmsManager.REGEX_PREFIX_DELIMITER);
252         for (String prefix : prefixes) {
253             if (message.startsWith(prefix)) {
254                 return true;
255             }
256         }
257         return false;
258     }
259 
removeRequestLocked(AppRequestInfo info)260     private void removeRequestLocked(AppRequestInfo info) {
261         mTokenMap.remove(info.token);
262         mPackageMap.remove(info.packageName);
263     }
264 
addRequestLocked(AppRequestInfo info)265     private void addRequestLocked(AppRequestInfo info) {
266         mTokenMap.put(info.token, info);
267         mPackageMap.put(info.packageName, info);
268     }
269 
270     private final class AppRequestInfo {
271         public final String packageName;
272         public final PendingIntent pendingIntent;
273         public final String token;
274         public final long timestamp;
275         public final String prefixes;
276         public final int subId;
277         public final boolean packageBasedToken;
278 
AppRequestInfo(String packageName, PendingIntent pendingIntent, String token)279         AppRequestInfo(String packageName, PendingIntent pendingIntent, String token) {
280           this(packageName, pendingIntent, token, null,
281                   SubscriptionManager.INVALID_SUBSCRIPTION_ID, false);
282         }
283 
AppRequestInfo(String packageName, PendingIntent pendingIntent, String token, String prefixes, int subId, boolean packageBasedToken)284         AppRequestInfo(String packageName, PendingIntent pendingIntent, String token,
285                 String prefixes, int subId, boolean packageBasedToken) {
286             this.packageName = packageName;
287             this.pendingIntent = pendingIntent;
288             this.token = token;
289             this.timestamp = System.currentTimeMillis();
290             this.prefixes = prefixes;
291             this.subId = subId;
292             this.packageBasedToken = packageBasedToken;
293         }
294     }
295 
296 }
297