diff options
| -rw-r--r-- | api/current.txt | 11 | ||||
| -rw-r--r-- | api/system-current.txt | 11 | ||||
| -rw-r--r-- | api/test-current.txt | 16 | ||||
| -rw-r--r-- | core/java/android/service/autofill/AutofillService.java | 2 | ||||
| -rw-r--r-- | core/java/android/service/autofill/InternalSanitizer.java | 38 | ||||
| -rw-r--r-- | core/java/android/service/autofill/Sanitizer.java | 26 | ||||
| -rw-r--r-- | core/java/android/service/autofill/SaveInfo.java | 106 | ||||
| -rw-r--r-- | core/java/android/service/autofill/TextValueSanitizer.java | 122 | ||||
| -rw-r--r-- | services/autofill/java/com/android/server/autofill/Session.java | 54 |
9 files changed, 384 insertions, 2 deletions
diff --git a/api/current.txt b/api/current.txt index 25cac5c2a16e..385aadcc6e41 100644 --- a/api/current.txt +++ b/api/current.txt @@ -37221,6 +37221,9 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.RegexValidator> CREATOR; } + public abstract interface Sanitizer { + } + public final class SaveCallback { method public void onFailure(java.lang.CharSequence); method public void onSuccess(); @@ -37244,6 +37247,7 @@ package android.service.autofill { public static final class SaveInfo.Builder { ctor public SaveInfo.Builder(int, android.view.autofill.AutofillId[]); ctor public SaveInfo.Builder(int); + method public android.service.autofill.SaveInfo.Builder addSanitizer(android.service.autofill.Sanitizer, android.view.autofill.AutofillId...); method public android.service.autofill.SaveInfo build(); method public android.service.autofill.SaveInfo.Builder setCustomDescription(android.service.autofill.CustomDescription); method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence); @@ -37262,6 +37266,13 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.SaveRequest> CREATOR; } + public final class TextValueSanitizer implements android.os.Parcelable android.service.autofill.Sanitizer { + ctor public TextValueSanitizer(java.util.regex.Pattern, java.lang.String); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.TextValueSanitizer> CREATOR; + } + public abstract interface Transformation { } diff --git a/api/system-current.txt b/api/system-current.txt index 82cdbaf541f7..a63cd64b4bd3 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -40316,6 +40316,9 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.RegexValidator> CREATOR; } + public abstract interface Sanitizer { + } + public final class SaveCallback { method public void onFailure(java.lang.CharSequence); method public void onSuccess(); @@ -40339,6 +40342,7 @@ package android.service.autofill { public static final class SaveInfo.Builder { ctor public SaveInfo.Builder(int, android.view.autofill.AutofillId[]); ctor public SaveInfo.Builder(int); + method public android.service.autofill.SaveInfo.Builder addSanitizer(android.service.autofill.Sanitizer, android.view.autofill.AutofillId...); method public android.service.autofill.SaveInfo build(); method public android.service.autofill.SaveInfo.Builder setCustomDescription(android.service.autofill.CustomDescription); method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence); @@ -40357,6 +40361,13 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.SaveRequest> CREATOR; } + public final class TextValueSanitizer implements android.os.Parcelable android.service.autofill.Sanitizer { + ctor public TextValueSanitizer(java.util.regex.Pattern, java.lang.String); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.TextValueSanitizer> CREATOR; + } + public abstract interface Transformation { } diff --git a/api/test-current.txt b/api/test-current.txt index 34b663be5db6..3a6128f09535 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -37497,6 +37497,10 @@ package android.service.autofill { method public android.service.autofill.ImageTransformation build(); } + public abstract class InternalSanitizer implements android.os.Parcelable android.service.autofill.Sanitizer { + ctor public InternalSanitizer(); + } + public final class LuhnChecksumValidator implements android.os.Parcelable android.service.autofill.Validator { ctor public LuhnChecksumValidator(android.view.autofill.AutofillId...); method public int describeContents(); @@ -37513,6 +37517,9 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.RegexValidator> CREATOR; } + public abstract interface Sanitizer { + } + public final class SaveCallback { method public void onFailure(java.lang.CharSequence); method public void onSuccess(); @@ -37536,6 +37543,7 @@ package android.service.autofill { public static final class SaveInfo.Builder { ctor public SaveInfo.Builder(int, android.view.autofill.AutofillId[]); ctor public SaveInfo.Builder(int); + method public android.service.autofill.SaveInfo.Builder addSanitizer(android.service.autofill.Sanitizer, android.view.autofill.AutofillId...); method public android.service.autofill.SaveInfo build(); method public android.service.autofill.SaveInfo.Builder setCustomDescription(android.service.autofill.CustomDescription); method public android.service.autofill.SaveInfo.Builder setDescription(java.lang.CharSequence); @@ -37554,6 +37562,14 @@ package android.service.autofill { field public static final android.os.Parcelable.Creator<android.service.autofill.SaveRequest> CREATOR; } + public final class TextValueSanitizer extends android.service.autofill.InternalSanitizer implements android.os.Parcelable android.service.autofill.Sanitizer { + ctor public TextValueSanitizer(java.util.regex.Pattern, java.lang.String); + method public int describeContents(); + method public android.view.autofill.AutofillValue sanitize(android.view.autofill.AutofillValue); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.TextValueSanitizer> CREATOR; + } + public abstract interface Transformation { } diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java index 045c83304097..ae7245a7ba31 100644 --- a/core/java/android/service/autofill/AutofillService.java +++ b/core/java/android/service/autofill/AutofillService.java @@ -187,7 +187,7 @@ import com.android.internal.os.SomeArgs; * protect a dataset that contains sensitive information by requiring dataset authentication * (see {@link Dataset.Builder#setAuthentication(android.content.IntentSender)}), and to include * info about the "primary" field of the partition in the custom presentation for "secondary" - * fields — that would prevent a malicious app from getting the "primary" fields without the + * fields—that would prevent a malicious app from getting the "primary" fields without the * user realizing they're being released (for example, a malicious app could have fields for a * credit card number, verification code, and expiration date crafted in a way that just the latter * is visible; by explicitly indicating the expiration date is related to a given credit card diff --git a/core/java/android/service/autofill/InternalSanitizer.java b/core/java/android/service/autofill/InternalSanitizer.java new file mode 100644 index 000000000000..95d2f660b24b --- /dev/null +++ b/core/java/android/service/autofill/InternalSanitizer.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 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 android.service.autofill; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcelable; +import android.view.autofill.AutofillValue; + +/** + * Superclass of all sanitizers the system understands. As this is not public all public subclasses + * have to implement {@link Sanitizer} again. + * + * @hide + */ +@TestApi +public abstract class InternalSanitizer implements Sanitizer, Parcelable { + + /** + * Sanitizes an {@link AutofillValue}. + * + * @hide + */ + public abstract AutofillValue sanitize(@NonNull AutofillValue value); +} diff --git a/core/java/android/service/autofill/Sanitizer.java b/core/java/android/service/autofill/Sanitizer.java new file mode 100644 index 000000000000..38757ac7408b --- /dev/null +++ b/core/java/android/service/autofill/Sanitizer.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017 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 android.service.autofill; + +/** + * Helper class used to sanitize user input before using it in a save request. + * + * <p>Typically used to avoid displaying the save UI for values that are autofilled but reformatted + * by the app—for example, if the autofill service sends a credit card number + * value as "004815162342108" and the app automatically changes it to "0048 1516 2342 108". + */ +public interface Sanitizer { +} diff --git a/core/java/android/service/autofill/SaveInfo.java b/core/java/android/service/autofill/SaveInfo.java index e0a073050b6b..1b9240cc0943 100644 --- a/core/java/android/service/autofill/SaveInfo.java +++ b/core/java/android/service/autofill/SaveInfo.java @@ -25,6 +25,8 @@ import android.app.Activity; import android.content.IntentSender; import android.os.Parcel; import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.DebugUtils; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; @@ -232,6 +234,8 @@ public final class SaveInfo implements Parcelable { private final int mFlags; private final CustomDescription mCustomDescription; private final InternalValidator mValidator; + private final InternalSanitizer[] mSanitizerKeys; + private final AutofillId[][] mSanitizerValues; private SaveInfo(Builder builder) { mType = builder.mType; @@ -243,6 +247,18 @@ public final class SaveInfo implements Parcelable { mFlags = builder.mFlags; mCustomDescription = builder.mCustomDescription; mValidator = builder.mValidator; + if (builder.mSanitizers == null) { + mSanitizerKeys = null; + mSanitizerValues = null; + } else { + final int size = builder.mSanitizers.size(); + mSanitizerKeys = new InternalSanitizer[size]; + mSanitizerValues = new AutofillId[size][]; + for (int i = 0; i < size; i++) { + mSanitizerKeys[i] = builder.mSanitizers.keyAt(i); + mSanitizerValues[i] = builder.mSanitizers.valueAt(i); + } + } } /** @hide */ @@ -292,6 +308,18 @@ public final class SaveInfo implements Parcelable { return mValidator; } + /** @hide */ + @Nullable + public InternalSanitizer[] getSanitizerKeys() { + return mSanitizerKeys; + } + + /** @hide */ + @Nullable + public AutofillId[][] getSanitizerValues() { + return mSanitizerValues; + } + /** * A builder for {@link SaveInfo} objects. */ @@ -307,6 +335,9 @@ public final class SaveInfo implements Parcelable { private int mFlags; private CustomDescription mCustomDescription; private InternalValidator mValidator; + private ArrayMap<InternalSanitizer, AutofillId[]> mSanitizers; + // Set used to validate against duplicate ids. + private ArraySet<AutofillId> mSanitizerIds; /** * Creates a new builder. @@ -530,6 +561,61 @@ public final class SaveInfo implements Parcelable { } /** + * Adds a sanitizer for one or more field. + * + * <p>When a sanitizer is set for a field, the {@link AutofillValue} is sent to the + * sanitizer before a save request is <a href="#TriggeringSaveRequest">triggered</a>. + * + * <p>Typically used to avoid displaying the save UI for values that are autofilled but + * reformattedby the app. For example, to remove spaces between every 4 digits of a + * credit card number: + * + * <pre class="prettyprint"> + * builder.addSanitizer( + * new TextValueSanitizer(Pattern.compile("^(\\d{4}\s?\\d{4}\s?\\d{4}\s?\\d{4})$"), + * "$1$2$3$4"), ccNumberId); + * </pre> + * + * <p>The same sanitizer can be reused to sanitize multiple fields. For example, to trim + * both the username and password fields: + * + * <pre class="prettyprint"> + * builder.addSanitizer( + * new TextValueSanitizer(Pattern.compile("^\\s*(.*)\\s*$"), "$1"), + * usernameId, passwordId); + * </pre> + * + * @param sanitizer an implementation provided by the Android System. + * @param ids id of fields whose value will be sanitized. + * @return this builder. + * + * @throws IllegalArgumentException if a sanitizer for any of the {@code ids} has already + * been added or if {@code ids} is empty. + */ + public @NonNull Builder addSanitizer(@NonNull Sanitizer sanitizer, + @NonNull AutofillId... ids) { + throwIfDestroyed(); + Preconditions.checkArgument(!ArrayUtils.isEmpty(ids), "ids cannot be empty or null"); + Preconditions.checkArgument((sanitizer instanceof InternalSanitizer), + "not provided by Android System: " + sanitizer); + + if (mSanitizers == null) { + mSanitizers = new ArrayMap<>(); + mSanitizerIds = new ArraySet<>(ids.length); + } + + // Check for duplicates first. + for (AutofillId id : ids) { + Preconditions.checkArgument(!mSanitizerIds.contains(id), "already added %s", id); + mSanitizerIds.add(id); + } + + mSanitizers.put((InternalSanitizer) sanitizer, ids); + + return this; + } + + /** * Builds a new {@link SaveInfo} instance. * * @throws IllegalStateException if no @@ -569,6 +655,10 @@ public final class SaveInfo implements Parcelable { .append(", mFlags=").append(mFlags) .append(", mCustomDescription=").append(mCustomDescription) .append(", validation=").append(mValidator) + .append(", sanitizerKeys=") + .append(mSanitizerKeys == null ? "N/A:" : mSanitizerKeys.length) + .append(", sanitizerValues=") + .append(mSanitizerValues == null ? "N/A:" : mSanitizerValues.length) .append("]").toString(); } @@ -591,6 +681,12 @@ public final class SaveInfo implements Parcelable { parcel.writeCharSequence(mDescription); parcel.writeParcelable(mCustomDescription, flags); parcel.writeParcelable(mValidator, flags); + parcel.writeParcelableArray(mSanitizerKeys, flags); + if (mSanitizerKeys != null) { + for (int i = 0; i < mSanitizerValues.length; i++) { + parcel.writeParcelableArray(mSanitizerValues[i], flags); + } + } parcel.writeInt(mFlags); } @@ -621,6 +717,16 @@ public final class SaveInfo implements Parcelable { if (validator != null) { builder.setValidator(validator); } + final InternalSanitizer[] sanitizers = + parcel.readParcelableArray(null, InternalSanitizer.class); + if (sanitizers != null) { + final int size = sanitizers.length; + for (int i = 0; i < size; i++) { + final AutofillId[] autofillIds = + parcel.readParcelableArray(null, AutofillId.class); + builder.addSanitizer(sanitizers[i], autofillIds); + } + } builder.setFlags(parcel.readInt()); return builder.build(); } diff --git a/core/java/android/service/autofill/TextValueSanitizer.java b/core/java/android/service/autofill/TextValueSanitizer.java new file mode 100644 index 000000000000..12e85b1db490 --- /dev/null +++ b/core/java/android/service/autofill/TextValueSanitizer.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2017 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 android.service.autofill; + +import static android.view.autofill.Helper.sDebug; + +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Slog; +import android.view.autofill.AutofillValue; + +import com.android.internal.util.Preconditions; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Sanitizes a text {@link AutofillValue} using a regular expression (regex) substitution. + * + * <p>For example, to remove spaces from groups of 4-digits in a credit card: + * + * <pre class="prettyprint"> + * new TextValueSanitizer(Pattern.compile("^(\\d{4}\s?\\d{4}\s?\\d{4}\s?\\d{4})$"), "$1$2$3$4") + * </pre> + */ +public final class TextValueSanitizer extends InternalSanitizer implements + Sanitizer, Parcelable { + private static final String TAG = "TextValueSanitizer"; + + private final Pattern mRegex; + private final String mSubst; + + /** + * Default constructor. + * + * @param regex regular expression with groups (delimited by {@code (} and {@code (}) that + * are used to substitute parts of the {@link AutofillValue#getTextValue() text value}. + * @param subst the string that substitutes the matched regex, using {@code $} for + * group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc). + */ + public TextValueSanitizer(@NonNull Pattern regex, @NonNull String subst) { + mRegex = Preconditions.checkNotNull(regex); + mSubst = Preconditions.checkNotNull(subst); + } + + /** @hide */ + @Override + @TestApi + public AutofillValue sanitize(@NonNull AutofillValue value) { + if (value == null) { + Slog.w(TAG, "sanitize() called with null value"); + return null; + } + if (!value.isText()) return value; + + final CharSequence text = value.getTextValue(); + + try { + final Matcher matcher = mRegex.matcher(text); + if (!matcher.matches()) return value; + + final CharSequence sanitized = matcher.replaceAll(mSubst); + return AutofillValue.forText(sanitized); + } catch (Exception e) { + Slog.w(TAG, "Exception evaluating " + mRegex + "/" + mSubst + ": " + e); + return value; + } + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + if (!sDebug) return super.toString(); + + return "TextValueSanitizer: [regex=" + mRegex + ", subst=" + mSubst + "]"; + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeSerializable(mRegex); + parcel.writeString(mSubst); + } + + public static final Parcelable.Creator<TextValueSanitizer> CREATOR = + new Parcelable.Creator<TextValueSanitizer>() { + @Override + public TextValueSanitizer createFromParcel(Parcel parcel) { + return new TextValueSanitizer((Pattern) parcel.readSerializable(), parcel.readString()); + } + + @Override + public TextValueSanitizer[] newArray(int size) { + return new TextValueSanitizer[size]; + } + }; +} diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 11478fe4f31a..ed00ffed4f63 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -56,9 +56,11 @@ import android.service.autofill.Dataset; import android.service.autofill.FillContext; import android.service.autofill.FillRequest; import android.service.autofill.FillResponse; +import android.service.autofill.InternalSanitizer; import android.service.autofill.InternalValidator; import android.service.autofill.SaveInfo; import android.service.autofill.SaveRequest; +import android.service.autofill.Transformation; import android.service.autofill.ValueFinder; import android.util.ArrayMap; import android.util.ArraySet; @@ -856,6 +858,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return true; } + final ArrayMap<AutofillId, InternalSanitizer> sanitizers = createSanitizers(saveInfo); + // Cache used to make sure changed fields do not belong to a dataset. final ArrayMap<AutofillId, AutofillValue> currentValues = new ArrayMap<>(); final ArraySet<AutofillId> allIds = new ArraySet<>(); @@ -895,6 +899,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState break; } } + value = getSanitizedValue(sanitizers, id, value); currentValues.put(id, value); final AutofillValue filledValue = viewState.getAutofilledValue(); @@ -1037,6 +1042,48 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return true; } + @Nullable + private ArrayMap<AutofillId, InternalSanitizer> createSanitizers(@Nullable SaveInfo saveInfo) { + if (saveInfo == null) return null; + + final InternalSanitizer[] sanitizerKeys = saveInfo.getSanitizerKeys(); + if (sanitizerKeys == null) return null; + + final int size = sanitizerKeys.length ; + final ArrayMap<AutofillId, InternalSanitizer> sanitizers = new ArrayMap<>(size); + if (sDebug) Slog.d(TAG, "Service provided " + size + " sanitizers"); + final AutofillId[][] sanitizerValues = saveInfo.getSanitizerValues(); + for (int i = 0; i < size; i++) { + final InternalSanitizer sanitizer = sanitizerKeys[i]; + final AutofillId[] ids = sanitizerValues[i]; + if (sDebug) { + Slog.d(TAG, "sanitizer #" + i + " (" + sanitizer + ") for ids " + + Arrays.toString(ids)); + } + for (AutofillId id : ids) { + sanitizers.put(id, sanitizer); + } + } + return sanitizers; + } + + @NonNull + private AutofillValue getSanitizedValue( + @Nullable ArrayMap<AutofillId, InternalSanitizer> sanitizers, + @NonNull AutofillId id, + @NonNull AutofillValue value) { + if (sanitizers == null) return value; + + final InternalSanitizer sanitizer = sanitizers.get(id); + if (sanitizer == null) { + return value; + } + + final AutofillValue sanitized = sanitizer.sanitize(value); + if (sDebug) Slog.d(TAG, "Value for " + id + "(" + value + ") sanitized to " + sanitized); + return sanitized; + } + /** * Returns whether the session is currently showing the save UI */ @@ -1100,6 +1147,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } + final ArrayMap<AutofillId, InternalSanitizer> sanitizers = + createSanitizers(getSaveInfoLocked()); + final int numContexts = mContexts.size(); for (int contextNum = 0; contextNum < numContexts; contextNum++) { @@ -1126,7 +1176,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } if (sVerbose) Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value); - node.updateAutofillValue(value); + final AutofillValue sanitizedValue = getSanitizedValue(sanitizers, id, value); + + node.updateAutofillValue(sanitizedValue); } // Sanitize structure before it's sent to service. |