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