• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 android.content.integrity;
18 
19 import static com.android.internal.util.Preconditions.checkArgument;
20 
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import java.lang.annotation.Retention;
30 import java.lang.annotation.RetentionPolicy;
31 import java.nio.charset.StandardCharsets;
32 import java.security.MessageDigest;
33 import java.security.NoSuchAlgorithmException;
34 import java.util.Collections;
35 import java.util.List;
36 import java.util.Objects;
37 
38 /**
39  * Represents a simple formula consisting of an app install metadata field and a value.
40  *
41  * <p>Instances of this class are immutable.
42  *
43  * @hide
44  */
45 @VisibleForTesting
46 public abstract class AtomicFormula extends IntegrityFormula {
47 
48     /** @hide */
49     @IntDef(
50             value = {
51                 PACKAGE_NAME,
52                 APP_CERTIFICATE,
53                 INSTALLER_NAME,
54                 INSTALLER_CERTIFICATE,
55                 VERSION_CODE,
56                 PRE_INSTALLED,
57                 STAMP_TRUSTED,
58                 STAMP_CERTIFICATE_HASH,
59             })
60     @Retention(RetentionPolicy.SOURCE)
61     public @interface Key {}
62 
63     /** @hide */
64     @IntDef(value = {EQ, GT, GTE})
65     @Retention(RetentionPolicy.SOURCE)
66     public @interface Operator {}
67 
68     /**
69      * Package name of the app.
70      *
71      * <p>Can only be used in {@link StringAtomicFormula}.
72      */
73     public static final int PACKAGE_NAME = 0;
74 
75     /**
76      * SHA-256 of the app certificate of the app.
77      *
78      * <p>Can only be used in {@link StringAtomicFormula}.
79      */
80     public static final int APP_CERTIFICATE = 1;
81 
82     /**
83      * Package name of the installer. Will be empty string if installed by the system (e.g., adb).
84      *
85      * <p>Can only be used in {@link StringAtomicFormula}.
86      */
87     public static final int INSTALLER_NAME = 2;
88 
89     /**
90      * SHA-256 of the cert of the installer. Will be empty string if installed by the system (e.g.,
91      * adb).
92      *
93      * <p>Can only be used in {@link StringAtomicFormula}.
94      */
95     public static final int INSTALLER_CERTIFICATE = 3;
96 
97     /**
98      * Version code of the app.
99      *
100      * <p>Can only be used in {@link LongAtomicFormula}.
101      */
102     public static final int VERSION_CODE = 4;
103 
104     /**
105      * If the app is pre-installed on the device.
106      *
107      * <p>Can only be used in {@link BooleanAtomicFormula}.
108      */
109     public static final int PRE_INSTALLED = 5;
110 
111     /**
112      * If the APK has an embedded trusted stamp.
113      *
114      * <p>Can only be used in {@link BooleanAtomicFormula}.
115      */
116     public static final int STAMP_TRUSTED = 6;
117 
118     /**
119      * SHA-256 of the certificate used to sign the stamp embedded in the APK.
120      *
121      * <p>Can only be used in {@link StringAtomicFormula}.
122      */
123     public static final int STAMP_CERTIFICATE_HASH = 7;
124 
125     public static final int EQ = 0;
126     public static final int GT = 1;
127     public static final int GTE = 2;
128 
129     private final @Key int mKey;
130 
AtomicFormula(@ey int key)131     public AtomicFormula(@Key int key) {
132         checkArgument(isValidKey(key), "Unknown key: %d", key);
133         mKey = key;
134     }
135 
136     /** An {@link AtomicFormula} with an key and long value. */
137     public static final class LongAtomicFormula extends AtomicFormula implements Parcelable {
138         private final Long mValue;
139         private final @Operator Integer mOperator;
140 
141         /**
142          * Constructs an empty {@link LongAtomicFormula}. This should only be used as a base.
143          *
144          * <p>This formula will always return false.
145          *
146          * @throws IllegalArgumentException if {@code key} cannot be used with long value
147          */
LongAtomicFormula(@ey int key)148         public LongAtomicFormula(@Key int key) {
149             super(key);
150             checkArgument(
151                     key == VERSION_CODE,
152                     "Key %s cannot be used with LongAtomicFormula", keyToString(key));
153             mValue = null;
154             mOperator = null;
155         }
156 
157         /**
158          * Constructs a new {@link LongAtomicFormula}.
159          *
160          * <p>This formula will hold if and only if the corresponding information of an install
161          * specified by {@code key} is of the correct relationship to {@code value} as specified by
162          * {@code operator}.
163          *
164          * @throws IllegalArgumentException if {@code key} cannot be used with long value
165          */
LongAtomicFormula(@ey int key, @Operator int operator, long value)166         public LongAtomicFormula(@Key int key, @Operator int operator, long value) {
167             super(key);
168             checkArgument(
169                     key == VERSION_CODE,
170                     "Key %s cannot be used with LongAtomicFormula", keyToString(key));
171             checkArgument(
172                     isValidOperator(operator), "Unknown operator: %d", operator);
173             mOperator = operator;
174             mValue = value;
175         }
176 
LongAtomicFormula(Parcel in)177         LongAtomicFormula(Parcel in) {
178             super(in.readInt());
179             mValue = in.readLong();
180             mOperator = in.readInt();
181         }
182 
183         @NonNull
184         public static final Creator<LongAtomicFormula> CREATOR =
185                 new Creator<LongAtomicFormula>() {
186                     @Override
187                     public LongAtomicFormula createFromParcel(Parcel in) {
188                         return new LongAtomicFormula(in);
189                     }
190 
191                     @Override
192                     public LongAtomicFormula[] newArray(int size) {
193                         return new LongAtomicFormula[size];
194                     }
195                 };
196 
197         @Override
getTag()198         public int getTag() {
199             return IntegrityFormula.LONG_ATOMIC_FORMULA_TAG;
200         }
201 
202         @Override
matches(AppInstallMetadata appInstallMetadata)203         public boolean matches(AppInstallMetadata appInstallMetadata) {
204             if (mValue == null || mOperator == null) {
205                 return false;
206             }
207 
208             long metadataValue = getLongMetadataValue(appInstallMetadata, getKey());
209             switch (mOperator) {
210                 case EQ:
211                     return metadataValue == mValue;
212                 case GT:
213                     return metadataValue > mValue;
214                 case GTE:
215                     return metadataValue >= mValue;
216                 default:
217                     throw new IllegalArgumentException(
218                             String.format("Unexpected operator %d", mOperator));
219             }
220         }
221 
222         @Override
isAppCertificateFormula()223         public boolean isAppCertificateFormula() {
224             return false;
225         }
226 
227         @Override
isInstallerFormula()228         public boolean isInstallerFormula() {
229             return false;
230         }
231 
232         @Override
toString()233         public String toString() {
234             if (mValue == null || mOperator == null) {
235                 return String.format("(%s)", keyToString(getKey()));
236             }
237             return String.format(
238                     "(%s %s %s)", keyToString(getKey()), operatorToString(mOperator), mValue);
239         }
240 
241         @Override
equals(@ullable Object o)242         public boolean equals(@Nullable Object o) {
243             if (this == o) {
244                 return true;
245             }
246             if (o == null || getClass() != o.getClass()) {
247                 return false;
248             }
249             LongAtomicFormula that = (LongAtomicFormula) o;
250             return getKey() == that.getKey()
251                     && mValue == that.mValue
252                     && mOperator == that.mOperator;
253         }
254 
255         @Override
hashCode()256         public int hashCode() {
257             return Objects.hash(getKey(), mOperator, mValue);
258         }
259 
260         @Override
describeContents()261         public int describeContents() {
262             return 0;
263         }
264 
265         @Override
writeToParcel(@onNull Parcel dest, int flags)266         public void writeToParcel(@NonNull Parcel dest, int flags) {
267             if (mValue == null || mOperator == null) {
268                 throw new IllegalStateException("Cannot write an empty LongAtomicFormula.");
269             }
270             dest.writeInt(getKey());
271             dest.writeLong(mValue);
272             dest.writeInt(mOperator);
273         }
274 
getValue()275         public Long getValue() {
276             return mValue;
277         }
278 
getOperator()279         public Integer getOperator() {
280             return mOperator;
281         }
282 
isValidOperator(int operator)283         private static boolean isValidOperator(int operator) {
284             return operator == EQ || operator == GT || operator == GTE;
285         }
286 
getLongMetadataValue(AppInstallMetadata appInstallMetadata, int key)287         private static long getLongMetadataValue(AppInstallMetadata appInstallMetadata, int key) {
288             switch (key) {
289                 case AtomicFormula.VERSION_CODE:
290                     return appInstallMetadata.getVersionCode();
291                 default:
292                     throw new IllegalStateException("Unexpected key in IntAtomicFormula" + key);
293             }
294         }
295     }
296 
297     /** An {@link AtomicFormula} with a key and string value. */
298     public static final class StringAtomicFormula extends AtomicFormula implements Parcelable {
299         private final String mValue;
300         // Indicates whether the value is the actual value or the hashed value.
301         private final Boolean mIsHashedValue;
302 
303         /**
304          * Constructs an empty {@link StringAtomicFormula}. This should only be used as a base.
305          *
306          * <p>An empty formula will always match to false.
307          *
308          * @throws IllegalArgumentException if {@code key} cannot be used with string value
309          */
StringAtomicFormula(@ey int key)310         public StringAtomicFormula(@Key int key) {
311             super(key);
312             checkArgument(
313                     key == PACKAGE_NAME
314                             || key == APP_CERTIFICATE
315                             || key == INSTALLER_CERTIFICATE
316                             || key == INSTALLER_NAME
317                             || key == STAMP_CERTIFICATE_HASH,
318                     "Key %s cannot be used with StringAtomicFormula", keyToString(key));
319             mValue = null;
320             mIsHashedValue = null;
321         }
322 
323         /**
324          * Constructs a new {@link StringAtomicFormula}.
325          *
326          * <p>This formula will hold if and only if the corresponding information of an install
327          * specified by {@code key} equals {@code value}.
328          *
329          * @throws IllegalArgumentException if {@code key} cannot be used with string value
330          */
StringAtomicFormula(@ey int key, @NonNull String value, boolean isHashed)331         public StringAtomicFormula(@Key int key, @NonNull String value, boolean isHashed) {
332             super(key);
333             checkArgument(
334                     key == PACKAGE_NAME
335                             || key == APP_CERTIFICATE
336                             || key == INSTALLER_CERTIFICATE
337                             || key == INSTALLER_NAME
338                             || key == STAMP_CERTIFICATE_HASH,
339                     "Key %s cannot be used with StringAtomicFormula", keyToString(key));
340             mValue = value;
341             mIsHashedValue = isHashed;
342         }
343 
344         /**
345          * Constructs a new {@link StringAtomicFormula} together with handling the necessary hashing
346          * for the given key.
347          *
348          * <p>The value will be automatically hashed with SHA256 and the hex digest will be computed
349          * when the key is PACKAGE_NAME or INSTALLER_NAME and the value is more than 32 characters.
350          *
351          * <p>The APP_CERTIFICATES, INSTALLER_CERTIFICATES, and STAMP_CERTIFICATE_HASH are always
352          * delivered in hashed form. So the isHashedValue is set to true by default.
353          *
354          * @throws IllegalArgumentException if {@code key} cannot be used with string value.
355          */
StringAtomicFormula(@ey int key, @NonNull String value)356         public StringAtomicFormula(@Key int key, @NonNull String value) {
357             super(key);
358             checkArgument(
359                     key == PACKAGE_NAME
360                             || key == APP_CERTIFICATE
361                             || key == INSTALLER_CERTIFICATE
362                             || key == INSTALLER_NAME
363                             || key == STAMP_CERTIFICATE_HASH,
364                     "Key %s cannot be used with StringAtomicFormula", keyToString(key));
365             mValue = hashValue(key, value);
366             mIsHashedValue =
367                     (key == APP_CERTIFICATE
368                                     || key == INSTALLER_CERTIFICATE
369                                     || key == STAMP_CERTIFICATE_HASH)
370                             || !mValue.equals(value);
371         }
372 
StringAtomicFormula(Parcel in)373         StringAtomicFormula(Parcel in) {
374             super(in.readInt());
375             mValue = in.readStringNoHelper();
376             mIsHashedValue = in.readByte() != 0;
377         }
378 
379         @NonNull
380         public static final Creator<StringAtomicFormula> CREATOR =
381                 new Creator<StringAtomicFormula>() {
382                     @Override
383                     public StringAtomicFormula createFromParcel(Parcel in) {
384                         return new StringAtomicFormula(in);
385                     }
386 
387                     @Override
388                     public StringAtomicFormula[] newArray(int size) {
389                         return new StringAtomicFormula[size];
390                     }
391                 };
392 
393         @Override
getTag()394         public int getTag() {
395             return IntegrityFormula.STRING_ATOMIC_FORMULA_TAG;
396         }
397 
398         @Override
matches(AppInstallMetadata appInstallMetadata)399         public boolean matches(AppInstallMetadata appInstallMetadata) {
400             if (mValue == null || mIsHashedValue == null) {
401                 return false;
402             }
403             return getMetadataValue(appInstallMetadata, getKey()).contains(mValue);
404         }
405 
406         @Override
isAppCertificateFormula()407         public boolean isAppCertificateFormula() {
408             return getKey() == APP_CERTIFICATE;
409         }
410 
411         @Override
isInstallerFormula()412         public boolean isInstallerFormula() {
413             return getKey() == INSTALLER_NAME || getKey() == INSTALLER_CERTIFICATE;
414         }
415 
416         @Override
toString()417         public String toString() {
418             if (mValue == null || mIsHashedValue == null) {
419                 return String.format("(%s)", keyToString(getKey()));
420             }
421             return String.format("(%s %s %s)", keyToString(getKey()), operatorToString(EQ), mValue);
422         }
423 
424         @Override
equals(@ullable Object o)425         public boolean equals(@Nullable Object o) {
426             if (this == o) {
427                 return true;
428             }
429             if (o == null || getClass() != o.getClass()) {
430                 return false;
431             }
432             StringAtomicFormula that = (StringAtomicFormula) o;
433             return getKey() == that.getKey() && Objects.equals(mValue, that.mValue);
434         }
435 
436         @Override
hashCode()437         public int hashCode() {
438             return Objects.hash(getKey(), mValue);
439         }
440 
441         @Override
describeContents()442         public int describeContents() {
443             return 0;
444         }
445 
446         @Override
writeToParcel(@onNull Parcel dest, int flags)447         public void writeToParcel(@NonNull Parcel dest, int flags) {
448             if (mValue == null || mIsHashedValue == null) {
449                 throw new IllegalStateException("Cannot write an empty StringAtomicFormula.");
450             }
451             dest.writeInt(getKey());
452             dest.writeStringNoHelper(mValue);
453             dest.writeByte((byte) (mIsHashedValue ? 1 : 0));
454         }
455 
getValue()456         public String getValue() {
457             return mValue;
458         }
459 
getIsHashedValue()460         public Boolean getIsHashedValue() {
461             return mIsHashedValue;
462         }
463 
getMetadataValue( AppInstallMetadata appInstallMetadata, int key)464         private static List<String> getMetadataValue(
465                 AppInstallMetadata appInstallMetadata, int key) {
466             switch (key) {
467                 case AtomicFormula.PACKAGE_NAME:
468                     return Collections.singletonList(appInstallMetadata.getPackageName());
469                 case AtomicFormula.APP_CERTIFICATE:
470                     return appInstallMetadata.getAppCertificates();
471                 case AtomicFormula.INSTALLER_CERTIFICATE:
472                     return appInstallMetadata.getInstallerCertificates();
473                 case AtomicFormula.INSTALLER_NAME:
474                     return Collections.singletonList(appInstallMetadata.getInstallerName());
475                 case AtomicFormula.STAMP_CERTIFICATE_HASH:
476                     return Collections.singletonList(appInstallMetadata.getStampCertificateHash());
477                 default:
478                     throw new IllegalStateException(
479                             "Unexpected key in StringAtomicFormula: " + key);
480             }
481         }
482 
hashValue(@ey int key, String value)483         private static String hashValue(@Key int key, String value) {
484             // Hash the string value if it is a PACKAGE_NAME or INSTALLER_NAME and the value is
485             // greater than 32 characters.
486             if (value.length() > 32) {
487                 if (key == PACKAGE_NAME || key == INSTALLER_NAME) {
488                     return hash(value);
489                 }
490             }
491             return value;
492         }
493 
hash(String value)494         private static String hash(String value) {
495             try {
496                 MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
497                 byte[] hashBytes = messageDigest.digest(value.getBytes(StandardCharsets.UTF_8));
498                 return IntegrityUtils.getHexDigest(hashBytes);
499             } catch (NoSuchAlgorithmException e) {
500                 throw new RuntimeException("SHA-256 algorithm not found", e);
501             }
502         }
503     }
504 
505     /** An {@link AtomicFormula} with a key and boolean value. */
506     public static final class BooleanAtomicFormula extends AtomicFormula implements Parcelable {
507         private final Boolean mValue;
508 
509         /**
510          * Constructs an empty {@link BooleanAtomicFormula}. This should only be used as a base.
511          *
512          * <p>An empty formula will always match to false.
513          *
514          * @throws IllegalArgumentException if {@code key} cannot be used with boolean value
515          */
BooleanAtomicFormula(@ey int key)516         public BooleanAtomicFormula(@Key int key) {
517             super(key);
518             checkArgument(
519                     key == PRE_INSTALLED || key == STAMP_TRUSTED,
520                     String.format(
521                             "Key %s cannot be used with BooleanAtomicFormula", keyToString(key)));
522             mValue = null;
523         }
524 
525         /**
526          * Constructs a new {@link BooleanAtomicFormula}.
527          *
528          * <p>This formula will hold if and only if the corresponding information of an install
529          * specified by {@code key} equals {@code value}.
530          *
531          * @throws IllegalArgumentException if {@code key} cannot be used with boolean value
532          */
BooleanAtomicFormula(@ey int key, boolean value)533         public BooleanAtomicFormula(@Key int key, boolean value) {
534             super(key);
535             checkArgument(
536                     key == PRE_INSTALLED || key == STAMP_TRUSTED,
537                     String.format(
538                             "Key %s cannot be used with BooleanAtomicFormula", keyToString(key)));
539             mValue = value;
540         }
541 
BooleanAtomicFormula(Parcel in)542         BooleanAtomicFormula(Parcel in) {
543             super(in.readInt());
544             mValue = in.readByte() != 0;
545         }
546 
547         @NonNull
548         public static final Creator<BooleanAtomicFormula> CREATOR =
549                 new Creator<BooleanAtomicFormula>() {
550                     @Override
551                     public BooleanAtomicFormula createFromParcel(Parcel in) {
552                         return new BooleanAtomicFormula(in);
553                     }
554 
555                     @Override
556                     public BooleanAtomicFormula[] newArray(int size) {
557                         return new BooleanAtomicFormula[size];
558                     }
559                 };
560 
561         @Override
getTag()562         public int getTag() {
563             return IntegrityFormula.BOOLEAN_ATOMIC_FORMULA_TAG;
564         }
565 
566         @Override
matches(AppInstallMetadata appInstallMetadata)567         public boolean matches(AppInstallMetadata appInstallMetadata) {
568             if (mValue == null) {
569                 return false;
570             }
571             return getBooleanMetadataValue(appInstallMetadata, getKey()) == mValue;
572         }
573 
574         @Override
isAppCertificateFormula()575         public boolean isAppCertificateFormula() {
576             return false;
577         }
578 
579         @Override
isInstallerFormula()580         public boolean isInstallerFormula() {
581             return false;
582         }
583 
584         @Override
toString()585         public String toString() {
586             if (mValue == null) {
587                 return String.format("(%s)", keyToString(getKey()));
588             }
589             return String.format("(%s %s %s)", keyToString(getKey()), operatorToString(EQ), mValue);
590         }
591 
592         @Override
equals(@ullable Object o)593         public boolean equals(@Nullable Object o) {
594             if (this == o) {
595                 return true;
596             }
597             if (o == null || getClass() != o.getClass()) {
598                 return false;
599             }
600             BooleanAtomicFormula that = (BooleanAtomicFormula) o;
601             return getKey() == that.getKey() && mValue == that.mValue;
602         }
603 
604         @Override
hashCode()605         public int hashCode() {
606             return Objects.hash(getKey(), mValue);
607         }
608 
609         @Override
describeContents()610         public int describeContents() {
611             return 0;
612         }
613 
614         @Override
writeToParcel(@onNull Parcel dest, int flags)615         public void writeToParcel(@NonNull Parcel dest, int flags) {
616             if (mValue == null) {
617                 throw new IllegalStateException("Cannot write an empty BooleanAtomicFormula.");
618             }
619             dest.writeInt(getKey());
620             dest.writeByte((byte) (mValue ? 1 : 0));
621         }
622 
getValue()623         public Boolean getValue() {
624             return mValue;
625         }
626 
getBooleanMetadataValue( AppInstallMetadata appInstallMetadata, int key)627         private static boolean getBooleanMetadataValue(
628                 AppInstallMetadata appInstallMetadata, int key) {
629             switch (key) {
630                 case AtomicFormula.PRE_INSTALLED:
631                     return appInstallMetadata.isPreInstalled();
632                 case AtomicFormula.STAMP_TRUSTED:
633                     return appInstallMetadata.isStampTrusted();
634                 default:
635                     throw new IllegalStateException(
636                             "Unexpected key in BooleanAtomicFormula: " + key);
637             }
638         }
639     }
640 
getKey()641     public int getKey() {
642         return mKey;
643     }
644 
keyToString(int key)645     static String keyToString(int key) {
646         switch (key) {
647             case PACKAGE_NAME:
648                 return "PACKAGE_NAME";
649             case APP_CERTIFICATE:
650                 return "APP_CERTIFICATE";
651             case VERSION_CODE:
652                 return "VERSION_CODE";
653             case INSTALLER_NAME:
654                 return "INSTALLER_NAME";
655             case INSTALLER_CERTIFICATE:
656                 return "INSTALLER_CERTIFICATE";
657             case PRE_INSTALLED:
658                 return "PRE_INSTALLED";
659             case STAMP_TRUSTED:
660                 return "STAMP_TRUSTED";
661             case STAMP_CERTIFICATE_HASH:
662                 return "STAMP_CERTIFICATE_HASH";
663             default:
664                 throw new IllegalArgumentException("Unknown key " + key);
665         }
666     }
667 
operatorToString(int op)668     static String operatorToString(int op) {
669         switch (op) {
670             case EQ:
671                 return "EQ";
672             case GT:
673                 return "GT";
674             case GTE:
675                 return "GTE";
676             default:
677                 throw new IllegalArgumentException("Unknown operator " + op);
678         }
679     }
680 
isValidKey(int key)681     private static boolean isValidKey(int key) {
682         return key == PACKAGE_NAME
683                 || key == APP_CERTIFICATE
684                 || key == VERSION_CODE
685                 || key == INSTALLER_NAME
686                 || key == INSTALLER_CERTIFICATE
687                 || key == PRE_INSTALLED
688                 || key == STAMP_TRUSTED
689                 || key == STAMP_CERTIFICATE_HASH;
690     }
691 }
692