1 /*
2  * Copyright 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 package android.app.blob;
17 
18 import static android.app.blob.XmlTags.ATTR_ALGO;
19 import static android.app.blob.XmlTags.ATTR_DIGEST;
20 import static android.app.blob.XmlTags.ATTR_EXPIRY_TIME;
21 import static android.app.blob.XmlTags.ATTR_LABEL;
22 import static android.app.blob.XmlTags.ATTR_TAG;
23 
24 import android.annotation.CurrentTimeMillisLong;
25 import android.annotation.NonNull;
26 import android.os.Parcel;
27 import android.os.Parcelable;
28 import android.util.Base64;
29 import android.util.IndentingPrintWriter;
30 
31 import com.android.internal.util.Preconditions;
32 import com.android.internal.util.XmlUtils;
33 
34 import org.xmlpull.v1.XmlPullParser;
35 import org.xmlpull.v1.XmlSerializer;
36 
37 import java.io.IOException;
38 import java.util.Arrays;
39 import java.util.Objects;
40 
41 /**
42  * An identifier to represent a blob.
43  */
44 // TODO: use datagen tool?
45 public final class BlobHandle implements Parcelable {
46     /** @hide */
47     public static final String ALGO_SHA_256 = "SHA-256";
48 
49     private static final String[] SUPPORTED_ALGOS = {
50             ALGO_SHA_256
51     };
52 
53     private static final int LIMIT_BLOB_TAG_LENGTH = 128; // characters
54     private static final int LIMIT_BLOB_LABEL_LENGTH = 100; // characters
55 
56     /**
57      * Cyrptographically secure hash algorithm used to generate hash of the blob this handle is
58      * representing.
59      *
60      * @hide
61      */
62     @NonNull public final String algorithm;
63 
64     /**
65      * Hash of the blob this handle is representing using {@link #algorithm}.
66      *
67      * @hide
68      */
69     @NonNull public final byte[] digest;
70 
71     /**
72      * Label of the blob that can be surfaced to the user.
73      * @hide
74      */
75     @NonNull public final CharSequence label;
76 
77     /**
78      * Time in milliseconds after which the blob should be invalidated and not
79      * allowed to be accessed by any other app, in {@link System#currentTimeMillis()} timebase.
80      *
81      * @hide
82      */
83     @CurrentTimeMillisLong public final long expiryTimeMillis;
84 
85     /**
86      * An opaque {@link String} associated with the blob.
87      *
88      * @hide
89      */
90     @NonNull public final String tag;
91 
BlobHandle(String algorithm, byte[] digest, CharSequence label, long expiryTimeMillis, String tag)92     private BlobHandle(String algorithm, byte[] digest, CharSequence label, long expiryTimeMillis,
93             String tag) {
94         this.algorithm = algorithm;
95         this.digest = digest;
96         this.label = label;
97         this.expiryTimeMillis = expiryTimeMillis;
98         this.tag = tag;
99     }
100 
BlobHandle(Parcel in)101     private BlobHandle(Parcel in) {
102         this.algorithm = in.readString();
103         this.digest = in.createByteArray();
104         this.label = in.readCharSequence();
105         this.expiryTimeMillis = in.readLong();
106         this.tag = in.readString();
107     }
108 
109     /** @hide */
create(@onNull String algorithm, @NonNull byte[] digest, @NonNull CharSequence label, @CurrentTimeMillisLong long expiryTimeMillis, @NonNull String tag)110     public static @NonNull BlobHandle create(@NonNull String algorithm, @NonNull byte[] digest,
111             @NonNull CharSequence label, @CurrentTimeMillisLong long expiryTimeMillis,
112             @NonNull String tag) {
113         final BlobHandle handle = new BlobHandle(algorithm, digest, label, expiryTimeMillis, tag);
114         handle.assertIsValid();
115         return handle;
116     }
117 
118     /**
119      * Create a new blob identifier.
120      *
121      * <p> For two objects of {@link BlobHandle} to be considered equal, the following arguments
122      * must be equal:
123      * <ul>
124      * <li> {@code digest}
125      * <li> {@code label}
126      * <li> {@code expiryTimeMillis}
127      * <li> {@code tag}
128      * </ul>
129      *
130      * @param digest the SHA-256 hash of the blob this is representing.
131      * @param label a label indicating what the blob is, that can be surfaced to the user.
132      *              The length of the label cannot be more than 100 characters. It is recommended
133      *              to keep this brief. This may be truncated and ellipsized if it is too long
134      *              to be displayed to the user.
135      * @param expiryTimeMillis the time in secs after which the blob should be invalidated and not
136      *                         allowed to be accessed by any other app,
137      *                         in {@link System#currentTimeMillis()} timebase or {@code 0} to
138      *                         indicate that there is no expiry time associated with this blob.
139      * @param tag an opaque {@link String} associated with the blob. The length of the tag
140      *            cannot be more than 128 characters.
141      *
142      * @return a new instance of {@link BlobHandle} object.
143      */
createWithSha256(@onNull byte[] digest, @NonNull CharSequence label, @CurrentTimeMillisLong long expiryTimeMillis, @NonNull String tag)144     public static @NonNull BlobHandle createWithSha256(@NonNull byte[] digest,
145             @NonNull CharSequence label, @CurrentTimeMillisLong long expiryTimeMillis,
146             @NonNull String tag) {
147         return create(ALGO_SHA_256, digest, label, expiryTimeMillis, tag);
148     }
149 
150     /**
151      * Returns the SHA-256 hash of the blob that this object is representing.
152      *
153      * @see #createWithSha256(byte[], CharSequence, long, String)
154      */
getSha256Digest()155     public @NonNull byte[] getSha256Digest() {
156         return digest;
157     }
158 
159     /**
160      * Returns the label associated with the blob that this object is representing.
161      *
162      * @see #createWithSha256(byte[], CharSequence, long, String)
163      */
getLabel()164     public @NonNull CharSequence getLabel() {
165         return label;
166     }
167 
168     /**
169      * Returns the expiry time in milliseconds of the blob that this object is representing, in
170      *         {@link System#currentTimeMillis()} timebase.
171      *
172      * @see #createWithSha256(byte[], CharSequence, long, String)
173      */
getExpiryTimeMillis()174     public @CurrentTimeMillisLong long getExpiryTimeMillis() {
175         return expiryTimeMillis;
176     }
177 
178     /**
179      * Returns the opaque {@link String} associated with the blob this object is representing.
180      *
181      * @see #createWithSha256(byte[], CharSequence, long, String)
182      */
getTag()183     public @NonNull String getTag() {
184         return tag;
185     }
186 
187     @Override
describeContents()188     public int describeContents() {
189         return 0;
190     }
191 
192     @Override
writeToParcel(@onNull Parcel dest, int flags)193     public void writeToParcel(@NonNull Parcel dest, int flags) {
194         dest.writeString(algorithm);
195         dest.writeByteArray(digest);
196         dest.writeCharSequence(label);
197         dest.writeLong(expiryTimeMillis);
198         dest.writeString(tag);
199     }
200 
201     @Override
equals(Object obj)202     public boolean equals(Object obj) {
203         if (this == obj) {
204             return true;
205         }
206         if (obj == null || !(obj instanceof BlobHandle)) {
207             return false;
208         }
209         final BlobHandle other = (BlobHandle) obj;
210         return this.algorithm.equals(other.algorithm)
211                 && Arrays.equals(this.digest, other.digest)
212                 && this.label.toString().equals(other.label.toString())
213                 && this.expiryTimeMillis == other.expiryTimeMillis
214                 && this.tag.equals(other.tag);
215     }
216 
217     @Override
hashCode()218     public int hashCode() {
219         return Objects.hash(algorithm, Arrays.hashCode(digest), label, expiryTimeMillis, tag);
220     }
221 
222     /** @hide */
dump(IndentingPrintWriter fout, boolean dumpFull)223     public void dump(IndentingPrintWriter fout, boolean dumpFull) {
224         if (dumpFull) {
225             fout.println("algo: " + algorithm);
226             fout.println("digest: " + (dumpFull ? encodeDigest(digest) : safeDigest(digest)));
227             fout.println("label: " + label);
228             fout.println("expiryMs: " + expiryTimeMillis);
229             fout.println("tag: " + tag);
230         } else {
231             fout.println(toString());
232         }
233     }
234 
235     /** @hide */
assertIsValid()236     public void assertIsValid() {
237         Preconditions.checkArgumentIsSupported(SUPPORTED_ALGOS, algorithm);
238         Preconditions.checkByteArrayNotEmpty(digest, "digest");
239         Preconditions.checkStringNotEmpty(label, "label must not be null");
240         Preconditions.checkArgument(label.length() <= LIMIT_BLOB_LABEL_LENGTH, "label too long");
241         Preconditions.checkArgumentNonnegative(expiryTimeMillis,
242                 "expiryTimeMillis must not be negative");
243         Preconditions.checkStringNotEmpty(tag, "tag must not be null");
244         Preconditions.checkArgument(tag.length() <= LIMIT_BLOB_TAG_LENGTH, "tag too long");
245     }
246 
247     @Override
toString()248     public String toString() {
249         return "BlobHandle {"
250                 + "algo:" + algorithm + ","
251                 + "digest:" + safeDigest(digest) + ","
252                 + "label:" + label + ","
253                 + "expiryMs:" + expiryTimeMillis + ","
254                 + "tag:" + tag
255                 + "}";
256     }
257 
258     /** @hide */
safeDigest(@onNull byte[] digest)259     public static String safeDigest(@NonNull byte[] digest) {
260         final String digestStr = encodeDigest(digest);
261         return digestStr.substring(0, 2) + ".." + digestStr.substring(digestStr.length() - 2);
262     }
263 
encodeDigest(@onNull byte[] digest)264     private static String encodeDigest(@NonNull byte[] digest) {
265         return Base64.encodeToString(digest, Base64.NO_WRAP);
266     }
267 
268     /** @hide */
isExpired()269     public boolean isExpired() {
270         return expiryTimeMillis != 0 && expiryTimeMillis < System.currentTimeMillis();
271     }
272 
273     public static final @NonNull Creator<BlobHandle> CREATOR = new Creator<BlobHandle>() {
274         @Override
275         public @NonNull BlobHandle createFromParcel(@NonNull Parcel source) {
276             return new BlobHandle(source);
277         }
278 
279         @Override
280         public @NonNull BlobHandle[] newArray(int size) {
281             return new BlobHandle[size];
282         }
283     };
284 
285     /** @hide */
writeToXml(@onNull XmlSerializer out)286     public void writeToXml(@NonNull XmlSerializer out) throws IOException {
287         XmlUtils.writeStringAttribute(out, ATTR_ALGO, algorithm);
288         XmlUtils.writeByteArrayAttribute(out, ATTR_DIGEST, digest);
289         XmlUtils.writeStringAttribute(out, ATTR_LABEL, label);
290         XmlUtils.writeLongAttribute(out, ATTR_EXPIRY_TIME, expiryTimeMillis);
291         XmlUtils.writeStringAttribute(out, ATTR_TAG, tag);
292     }
293 
294     /** @hide */
295     @NonNull
createFromXml(@onNull XmlPullParser in)296     public static BlobHandle createFromXml(@NonNull XmlPullParser in) throws IOException {
297         final String algo = XmlUtils.readStringAttribute(in, ATTR_ALGO);
298         final byte[] digest = XmlUtils.readByteArrayAttribute(in, ATTR_DIGEST);
299         final CharSequence label = XmlUtils.readStringAttribute(in, ATTR_LABEL);
300         final long expiryTimeMs = XmlUtils.readLongAttribute(in, ATTR_EXPIRY_TIME);
301         final String tag = XmlUtils.readStringAttribute(in, ATTR_TAG);
302 
303         return BlobHandle.create(algo, digest, label, expiryTimeMs, tag);
304     }
305 }
306