Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d820ed40 authored by Hongming Jin's avatar Hongming Jin Committed by Android (Google) Code Review
Browse files

Merge "Implementation of sms retriever api."

parents 0b4351b2 fc4452e4
Loading
Loading
Loading
Loading
+123 −6
Original line number Diff line number Diff line
@@ -16,21 +16,29 @@

package com.android.internal.telephony;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.provider.Telephony.Sms.Intents;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.telephony.SubscriptionManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Base64;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;

import java.security.SecureRandom;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;


/**
@@ -44,6 +52,7 @@ import java.util.Map;
public class AppSmsManager {
    private static final String LOG_TAG = "AppSmsManager";

    private static final long TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);
    private final SecureRandom mRandom;
    private final Context mContext;
    private final Object mLock = new Object();
@@ -91,6 +100,46 @@ public class AppSmsManager {
        return token;
    }

    /**
     * Create an app specific incoming SMS request for the the calling package.
     *
     * This method returns a token that if included in a subsequent incoming SMS message the
     * {@link Intents.SMS_RECEIVED_ACTION} intent will be delivered only to the calling package and
     * will not require the application have the {@link Manifest.permission#RECEIVE_SMS} permission.
     *
     * An app can only have one request at a time, if the app already has a request it will be
     * dropped and the new one will be added.
     *
     * @return Token to include in an SMS to have it delivered directly to the app.
     */
    public String createAppSpecificSmsTokenWithPackageInfo(int subId,
            @NonNull String callingPackageName,
            @Nullable String prefixes,
            @NonNull PendingIntent intent) {
        Preconditions.checkStringNotEmpty(callingPackageName,
                "callingPackageName cannot be null or empty.");
        Preconditions.checkNotNull(intent, "intent cannot be null");
        // Check calling uid matches callingpkg.
        AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
        appOps.checkPackage(Binder.getCallingUid(), callingPackageName);

        // Generate a token to store the request under.
        String token = PackageBasedTokenUtil.generateToken(mContext, callingPackageName);
        if (token != null) {
            synchronized (mLock) {
                // Only allow one request in flight from a package.
                if (mPackageMap.containsKey(callingPackageName)) {
                    removeRequestLocked(mPackageMap.get(callingPackageName));
                }
                // Store state.
                AppRequestInfo info = new AppRequestInfo(
                        callingPackageName, intent, token, prefixes, subId, true);
                addRequestLocked(info);
            }
        }
        return token;
    }

    /**
     * Handle an incoming SMS_DELIVER_ACTION intent if it is an app-only SMS.
     */
@@ -102,14 +151,27 @@ public class AppSmsManager {
        }

        synchronized (mLock) {
            AppRequestInfo info = findAppRequestInfoSmsIntentLocked(intent);
            removeExpiredTokenLocked();

            String message = extractMessage(intent);
            if (TextUtils.isEmpty(message)) {
                return false;
            }

            AppRequestInfo info = findAppRequestInfoSmsIntentLocked(message);
            if (info == null) {
                // The message didn't contain a token -- nothing to do.
                return false;
            }

            try {
                Intent fillIn = new Intent();
                fillIn.putExtras(intent.getExtras());
                Intent fillIn = new Intent()
                        .putExtras(intent.getExtras())
                        .putExtra(SmsManager.EXTRA_STATUS, SmsManager.RESULT_STATUS_SUCCESS)
                        .putExtra(SmsManager.EXTRA_SMS_MESSAGE, message)
                        .putExtra(SmsManager.EXTRA_SIM_SUBSCRIPTION_ID, info.subId)
                        .addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);

                info.pendingIntent.send(mContext, 0, fillIn);
            } catch (PendingIntent.CanceledException e) {
                // The pending intent is canceled, send this SMS as normal.
@@ -122,7 +184,31 @@ public class AppSmsManager {
        }
    }

    private AppRequestInfo findAppRequestInfoSmsIntentLocked(Intent intent) {
    private void removeExpiredTokenLocked() {
        final long currentTimeMillis = System.currentTimeMillis();

        final Set<String> keySet = mTokenMap.keySet();
        for (String token : keySet) {
            AppRequestInfo request = mTokenMap.get(token);
            if (request.packageBasedToken
                    && (currentTimeMillis - TIMEOUT_MILLIS > request.timestamp)) {
                // Send the provided intent with SMS retriever status
                try {
                    Intent fillIn = new Intent()
                            .putExtra(SmsManager.EXTRA_STATUS,
                                    SmsManager.RESULT_STATUS_TIMEOUT)
                            .addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
                    request.pendingIntent.send(mContext, 0, fillIn);
                } catch (PendingIntent.CanceledException e) {
                    // do nothing
                }

                removeRequestLocked(request);
            }
        }
    }

    private String extractMessage(Intent intent) {
        SmsMessage[] messages = Intents.getMessagesFromIntent(intent);
        if (messages == null) {
            return null;
@@ -135,11 +221,13 @@ public class AppSmsManager {
            fullMessageBuilder.append(message.getMessageBody());
        }

        String fullMessage = fullMessageBuilder.toString();
        return fullMessageBuilder.toString();
    }

    private AppRequestInfo findAppRequestInfoSmsIntentLocked(String fullMessage) {
        // Look for any tokens in the full message.
        for (String token : mTokenMap.keySet()) {
            if (fullMessage.contains(token)) {
            if (fullMessage.trim().contains(token) && hasPrefix(token, fullMessage)) {
                return mTokenMap.get(token);
            }
        }
@@ -152,6 +240,21 @@ public class AppSmsManager {
        return Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);
    }

    private boolean hasPrefix(String token, String message) {
        AppRequestInfo request = mTokenMap.get(token);
        if (TextUtils.isEmpty(request.prefixes)) {
            return true;
        }

        String[] prefixes = request.prefixes.split(SmsManager.REGEX_PREFIX_DELIMITER);
        for (String prefix : prefixes) {
            if (message.startsWith(prefix)) {
                return true;
            }
        }
        return false;
    }

    private void removeRequestLocked(AppRequestInfo info) {
        mTokenMap.remove(info.token);
        mPackageMap.remove(info.packageName);
@@ -166,11 +269,25 @@ public class AppSmsManager {
        public final String packageName;
        public final PendingIntent pendingIntent;
        public final String token;
        public final long timestamp;
        public final String prefixes;
        public final int subId;
        public final boolean packageBasedToken;

        AppRequestInfo(String packageName, PendingIntent pendingIntent, String token) {
          this(packageName, pendingIntent, token, null,
                  SubscriptionManager.INVALID_SUBSCRIPTION_ID, false);
        }

        AppRequestInfo(String packageName, PendingIntent pendingIntent, String token,
                String prefixes, int subId, boolean packageBasedToken) {
            this.packageName = packageName;
            this.pendingIntent = pendingIntent;
            this.token = token;
            this.timestamp = System.currentTimeMillis();
            this.prefixes = prefixes;
            this.subId = subId;
            this.packageBasedToken = packageBasedToken;
        }
    }

+109 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.telephony;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.util.Base64;
import android.util.Log;

import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;

/** Utility class for generating token, i.e., hash of package name and certificate. */
public class PackageBasedTokenUtil {
    private static final String TAG = "PackageBasedTokenUtil";
    private static final Charset CHARSET_UTF_8 = Charset.forName("UTF-8");
    private static final String HASH_TYPE = "SHA-256";
    private static final int NUM_HASHED_BYTES = 9; // 9 bytes = 72 bits = 12 Base64s

    static final int NUM_BASE64_CHARS = 11; // truncate 12 into 11 Base64 chars

    /**
     * Generate token and check collision with other packages.
     */
    public static String generateToken(Context context, String packageName) {
        PackageManager packageManager = context.getPackageManager();
        String token = generatePackageBasedToken(packageManager, packageName);

        // Check for token confliction
        List<PackageInfo> packages =
                packageManager.getInstalledPackages(PackageManager.GET_META_DATA);

        for (PackageInfo packageInfo : packages) {
            String otherPackageName = packageInfo.packageName;
            if (packageName.equals(otherPackageName)) {
                continue;
            }

            String otherToken = generatePackageBasedToken(packageManager, otherPackageName);
            if (token.equals(otherToken)) {
                Log.e(TAG, "token collides with other installed app.");
                token = null;
            }
        }

        return token;
    }

    private static String generatePackageBasedToken(
            PackageManager packageManager, String packageName) {
        String token = null;
        Signature[] signatures;

        try {
            // It is actually a certificate (public key), not a signature.
            signatures = packageManager.getPackageInfo(
                    packageName, PackageManager.GET_SIGNATURES).signatures;
        } catch (NameNotFoundException e) {
            Log.e(TAG, "Failed to find package with package name: " + packageName);
            return token;
        }

        if (signatures == null) {
            Log.e(TAG, "The certificates is missing.");
        } else {
            MessageDigest messageDigest;
            try {
                messageDigest = MessageDigest.getInstance(HASH_TYPE);
            } catch (NoSuchAlgorithmException e) {
                Log.e(TAG, "NoSuchAlgorithmException" + e);
                return null;
            }

            messageDigest.update(packageName.getBytes(CHARSET_UTF_8));
            String space = " ";
            messageDigest.update(space.getBytes(CHARSET_UTF_8));
            for (int i = 0; i < signatures.length; i++) {
                messageDigest.update(signatures[i].toCharsString().getBytes(CHARSET_UTF_8));
            }
            byte[] hashSignatures = messageDigest.digest();
            // truncated into NUM_HASHED_BYTES
            hashSignatures = Arrays.copyOf(hashSignatures, NUM_HASHED_BYTES);
            // encode into Base64
            token = Base64.encodeToString(hashSignatures, Base64.NO_PADDING | Base64.NO_WRAP);
            token = token.substring(0, NUM_BASE64_CHARS);
        }
        return token;
    }
}
+7 −0
Original line number Diff line number Diff line
@@ -395,6 +395,13 @@ public class UiccSmsController extends ISmsImplBase {
        }
    }

    @Override
    public String createAppSpecificSmsTokenWithPackageInfo(
            int subId, String callingPkg, String prefixes, PendingIntent intent) {
        return getPhone(subId).getAppSmsManager().createAppSpecificSmsTokenWithPackageInfo(
                subId, callingPkg, prefixes, intent);
    }

    @Override
    public String createAppSpecificSmsToken(int subId, String callingPkg, PendingIntent intent) {
        return getPhone(subId).getAppSmsManager().createAppSpecificSmsToken(callingPkg, intent);