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