Check access to user and password fields in APN db

When apps query the APN db and filter on the user or password fields,
make sure they have the proper permission for accessing APN data.

Bug: 124107808
Test: CTS
Merged-In: I96dc8d2b82571e31c958ae3d243c8d5daebea01c
Merged-In: Ia7be0ecdfca5e7adf6163dc015a413a98f1b9287
Change-Id: I96dc8d2b82571e31c958ae3d243c8d5daebea01c
(cherry picked from commit 9a74213831bac05c1bbc3183ab8e2f182e5e50cb)
diff --git a/src/com/android/providers/telephony/SqlTokenFinder.java b/src/com/android/providers/telephony/SqlTokenFinder.java
new file mode 100644
index 0000000..9be49fe
--- /dev/null
+++ b/src/com/android/providers/telephony/SqlTokenFinder.java
@@ -0,0 +1,170 @@
+/*
+ * 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.providers.telephony;
+
+import android.annotation.Nullable;
+
+import java.util.function.Consumer;
+
+/**
+ * Simple SQL parser to check statements for usage of prohibited/sensitive fields. Mostly copied
+ * from
+ * packages/providers/ContactsProvider/src/com/android/providers/contacts/sqlite/SqlChecker.java
+ */
+public class SqlTokenFinder{
+    private static boolean isAlpha(char ch) {
+        return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_');
+    }
+
+    private static boolean isNum(char ch) {
+        return ('0' <= ch && ch <= '9');
+    }
+
+    private static boolean isAlNum(char ch) {
+        return isAlpha(ch) || isNum(ch);
+    }
+
+    private static boolean isAnyOf(char ch, String set) {
+        return set.indexOf(ch) >= 0;
+    }
+
+    private static char peek(String s, int index) {
+        return index < s.length() ? s.charAt(index) : '\0';
+    }
+
+    /**
+     * SQL Tokenizer specialized to extract tokens from SQL (snippets).
+     *
+     * Based on sqlite3GetToken() in tokenzie.c in SQLite.
+     *
+     * Source for v3.8.6 (which android uses): http://www.sqlite.org/src/artifact/ae45399d6252b4d7
+     * (Latest source as of now: http://www.sqlite.org/src/artifact/78c8085bc7af1922)
+     *
+     * Also draft spec: http://www.sqlite.org/draft/tokenreq.html
+     */
+    public static void findTokens(@Nullable String sql, Consumer<String> checker) {
+        if (sql == null) {
+            return;
+        }
+        int pos = 0;
+        final int len = sql.length();
+        while (pos < len) {
+            final char ch = peek(sql, pos);
+
+            // Regular token.
+            if (isAlpha(ch)) {
+                final int start = pos;
+                pos++;
+                while (isAlNum(peek(sql, pos))) {
+                    pos++;
+                }
+                final int end = pos;
+
+                final String token = sql.substring(start, end);
+                checker.accept(token);
+
+                continue;
+            }
+
+            // Handle quoted tokens
+            if (isAnyOf(ch, "'\"`")) {
+                final int quoteStart = pos;
+                pos++;
+
+                for (;;) {
+                    pos = sql.indexOf(ch, pos);
+                    if (pos < 0) {
+                        throw new IllegalArgumentException("Unterminated quote in" + sql);
+                    }
+                    if (peek(sql, pos + 1) != ch) {
+                        break;
+                    }
+                    // Quoted quote char -- e.g. "abc""def" is a single string.
+                    pos += 2;
+                }
+                final int quoteEnd = pos;
+                pos++;
+
+                if (ch != '\'') {
+                    // Extract the token
+                    final String tokenUnquoted = sql.substring(quoteStart + 1, quoteEnd);
+
+                    final String token;
+
+                    // Unquote if needed. i.e. "aa""bb" -> aa"bb
+                    if (tokenUnquoted.indexOf(ch) >= 0) {
+                        token = tokenUnquoted.replaceAll(
+                                String.valueOf(ch) + ch, String.valueOf(ch));
+                    } else {
+                        token = tokenUnquoted;
+                    }
+                    checker.accept(token);
+                }
+                continue;
+            }
+            // Handle tokens enclosed in [...]
+            if (ch == '[') {
+                final int quoteStart = pos;
+                pos++;
+
+                pos = sql.indexOf(']', pos);
+                if (pos < 0) {
+                    throw new IllegalArgumentException("Unterminated quote in" + sql);
+                }
+                final int quoteEnd = pos;
+                pos++;
+
+                final String token = sql.substring(quoteStart + 1, quoteEnd);
+
+                checker.accept(token);
+                continue;
+            }
+
+            // Detect comments.
+            if (ch == '-' && peek(sql, pos + 1) == '-') {
+                pos += 2;
+                pos = sql.indexOf('\n', pos);
+                if (pos < 0) {
+                    // We disallow strings ending in an inline comment.
+                    throw new IllegalArgumentException("Unterminated comment in" + sql);
+                }
+                pos++;
+
+                continue;
+            }
+            if (ch == '/' && peek(sql, pos + 1) == '*') {
+                pos += 2;
+                pos = sql.indexOf("*/", pos);
+                if (pos < 0) {
+                    throw new IllegalArgumentException("Unterminated comment in" + sql);
+                }
+                pos += 2;
+
+                continue;
+            }
+
+            // Semicolon is never allowed.
+            if (ch == ';') {
+                throw new IllegalArgumentException("Semicolon is not allowed in " + sql);
+            }
+
+            // For this purpose, we can simply ignore other characters.
+            // (Note it doesn't handle the X'' literal properly and reports this X as a token,
+            // but that should be fine...)
+            pos++;
+        }
+    }
+}
diff --git a/src/com/android/providers/telephony/TelephonyProvider.java b/src/com/android/providers/telephony/TelephonyProvider.java
index df1e58c..8ff9121 100644
--- a/src/com/android/providers/telephony/TelephonyProvider.java
+++ b/src/com/android/providers/telephony/TelephonyProvider.java
@@ -100,6 +100,7 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
+import android.util.EventLog;
 import android.util.Log;
 import android.util.Pair;
 import android.util.Xml;
@@ -125,6 +126,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 
 public class TelephonyProvider extends ContentProvider
 {
@@ -2511,6 +2513,23 @@
         }
 
         if (match != URL_SIMINFO) {
+            // Determine if we need to do a check for fields in the selection
+            boolean selectionContainsSensitiveFields;
+            try {
+                selectionContainsSensitiveFields = containsSensitiveFields(selection);
+            } catch (Exception e) {
+                // Malformed sql, check permission anyway.
+                selectionContainsSensitiveFields = true;
+            }
+
+            if (selectionContainsSensitiveFields) {
+                try {
+                    checkPermission();
+                } catch (SecurityException e) {
+                    EventLog.writeEvent(0x534e4554, "124107808", Binder.getCallingUid());
+                    throw e;
+                }
+            }
             if (projectionIn != null) {
                 for (String column : projectionIn) {
                     if (TYPE.equals(column) ||
@@ -2555,6 +2574,21 @@
         return ret;
     }
 
+    private boolean containsSensitiveFields(String sqlStatement) {
+        try {
+            SqlTokenFinder.findTokens(sqlStatement, s -> {
+                switch (s) {
+                    case USER:
+                    case PASSWORD:
+                        throw new SecurityException();
+                }
+            });
+        } catch (SecurityException e) {
+            return true;
+        }
+        return false;
+    }
+
     @Override
     public String getType(Uri url)
     {