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.app.AppOpsManager; 20 import android.app.PendingIntent; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.os.Binder; 24 import android.provider.Telephony.Sms.Intents; 25 import android.telephony.SmsMessage; 26 import android.util.ArrayMap; 27 import android.util.Base64; 28 import android.util.Log; 29 30 import com.android.internal.annotations.GuardedBy; 31 32 import java.security.SecureRandom; 33 import java.util.Map; 34 35 36 /** 37 * Manager for app specific incoming SMS requests. This can be used to implement SMS based 38 * communication channels (e.g. for SMS based phone number verification) without needing the 39 * {@link Manifest.permission#RECEIVE_SMS} permission. 40 * 41 * {@link #createAppSpecificSmsRequest} allows an application to provide a {@link PendingIntent} 42 * that is triggered when an incoming SMS is received that contains the provided token. 43 */ 44 public class AppSmsManager { 45 private static final String LOG_TAG = "AppSmsManager"; 46 47 private final SecureRandom mRandom; 48 private final Context mContext; 49 private final Object mLock = new Object(); 50 51 @GuardedBy("mLock") 52 private final Map<String, AppRequestInfo> mTokenMap; 53 @GuardedBy("mLock") 54 private final Map<String, AppRequestInfo> mPackageMap; 55 AppSmsManager(Context context)56 public AppSmsManager(Context context) { 57 mRandom = new SecureRandom(); 58 mTokenMap = new ArrayMap<>(); 59 mPackageMap = new ArrayMap<>(); 60 mContext = context; 61 } 62 63 /** 64 * Create an app specific incoming SMS request for the the calling package. 65 * 66 * This method returns a token that if included in a subsequent incoming SMS message the 67 * {@link Intents.SMS_RECEIVED_ACTION} intent will be delivered only to the calling package and 68 * will not require the application have the {@link Manifest.permission#RECEIVE_SMS} permission. 69 * 70 * An app can only have one request at a time, if the app already has a request it will be 71 * dropped and the new one will be added. 72 * 73 * @return Token to include in an SMS to have it delivered directly to the app. 74 */ createAppSpecificSmsToken(String callingPkg, PendingIntent intent)75 public String createAppSpecificSmsToken(String callingPkg, PendingIntent intent) { 76 // Check calling uid matches callingpkg. 77 AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); 78 appOps.checkPackage(Binder.getCallingUid(), callingPkg); 79 80 // Generate a nonce to store the request under. 81 String token = generateNonce(); 82 synchronized (mLock) { 83 // Only allow one request in flight from a package. 84 if (mPackageMap.containsKey(callingPkg)) { 85 removeRequestLocked(mPackageMap.get(callingPkg)); 86 } 87 // Store state. 88 AppRequestInfo info = new AppRequestInfo(callingPkg, intent, token); 89 addRequestLocked(info); 90 } 91 return token; 92 } 93 94 /** 95 * Handle an incoming SMS_DELIVER_ACTION intent if it is an app-only SMS. 96 */ handleSmsReceivedIntent(Intent intent)97 public boolean handleSmsReceivedIntent(Intent intent) { 98 // Sanity check the action. 99 if (intent.getAction() != Intents.SMS_DELIVER_ACTION) { 100 Log.wtf(LOG_TAG, "Got intent with incorrect action: " + intent.getAction()); 101 return false; 102 } 103 104 synchronized (mLock) { 105 AppRequestInfo info = findAppRequestInfoSmsIntentLocked(intent); 106 if (info == null) { 107 // The message didn't contain a token -- nothing to do. 108 return false; 109 } 110 try { 111 Intent fillIn = new Intent(); 112 fillIn.putExtras(intent.getExtras()); 113 info.pendingIntent.send(mContext, 0, fillIn); 114 } catch (PendingIntent.CanceledException e) { 115 // The pending intent is canceled, send this SMS as normal. 116 removeRequestLocked(info); 117 return false; 118 } 119 120 removeRequestLocked(info); 121 return true; 122 } 123 } 124 findAppRequestInfoSmsIntentLocked(Intent intent)125 private AppRequestInfo findAppRequestInfoSmsIntentLocked(Intent intent) { 126 SmsMessage[] messages = Intents.getMessagesFromIntent(intent); 127 if (messages == null) { 128 return null; 129 } 130 StringBuilder fullMessageBuilder = new StringBuilder(); 131 for (SmsMessage message : messages) { 132 if (message.getMessageBody() == null) { 133 continue; 134 } 135 fullMessageBuilder.append(message.getMessageBody()); 136 } 137 138 String fullMessage = fullMessageBuilder.toString(); 139 140 // Look for any tokens in the full message. 141 for (String token : mTokenMap.keySet()) { 142 if (fullMessage.contains(token)) { 143 return mTokenMap.get(token); 144 } 145 } 146 return null; 147 } 148 generateNonce()149 private String generateNonce() { 150 byte[] bytes = new byte[8]; 151 mRandom.nextBytes(bytes); 152 return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); 153 } 154 removeRequestLocked(AppRequestInfo info)155 private void removeRequestLocked(AppRequestInfo info) { 156 mTokenMap.remove(info.token); 157 mPackageMap.remove(info.packageName); 158 } 159 addRequestLocked(AppRequestInfo info)160 private void addRequestLocked(AppRequestInfo info) { 161 mTokenMap.put(info.token, info); 162 mPackageMap.put(info.packageName, info); 163 } 164 165 private final class AppRequestInfo { 166 public final String packageName; 167 public final PendingIntent pendingIntent; 168 public final String token; 169 AppRequestInfo(String packageName, PendingIntent pendingIntent, String token)170 AppRequestInfo(String packageName, PendingIntent pendingIntent, String token) { 171 this.packageName = packageName; 172 this.pendingIntent = pendingIntent; 173 this.token = token; 174 } 175 } 176 177 } 178