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