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