/*
* Copyright (C) 2015 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 com.android.statementservice.retriever;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
/**
* Immutable value type that names an Android app asset.
*
* <p>An Android app can be named by its package name and certificate fingerprints using this JSON
* string: { "namespace": "android_app", "package_name": "[Java package name]",
* "sha256_cert_fingerprints": ["[SHA256 fingerprint of signing cert]", "[additional cert]", ...] }
*
* <p>For example, { "namespace": "android_app", "package_name": "com.test.mytestapp",
* "sha256_cert_fingerprints": ["24:D9:B4:57:A6:42:FB:E6:E5:B8:D6:9E:7B:2D:C2:D1:CB:D1:77:17:1D:7F:D4:A9:16:10:11:AB:92:B9:8F:3F"]
* }
*
* <p>Given a signed APK, Java 7's commandline keytool can compute the fingerprint using:
* {@code keytool -list -printcert -jarfile signed_app.apk}
*
* <p>Each entry in "sha256_cert_fingerprints" is a colon-separated hex string (e.g. 14:6D:E9:...)
* representing the certificate SHA-256 fingerprint.
*/
/* package private */ final class AndroidAppAsset extends AbstractAsset {
private static final String MISSING_FIELD_FORMAT_STRING = "Expected %s to be set.";
private static final String MISSING_APPCERTS_FORMAT_STRING =
"Expected %s to be non-empty array.";
private static final String APPCERT_NOT_STRING_FORMAT_STRING = "Expected all %s to be strings.";
private final List<String> mCertFingerprints;
private final String mPackageName;
public List<String> getCertFingerprints() {
return Collections.unmodifiableList(mCertFingerprints);
}
public String getPackageName() {
return mPackageName;
}
@Override
public String toJson() {
AssetJsonWriter writer = new AssetJsonWriter();
writer.writeFieldLower(Utils.NAMESPACE_FIELD, Utils.NAMESPACE_ANDROID_APP);
writer.writeFieldLower(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME, mPackageName);
writer.writeArrayUpper(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS, mCertFingerprints);
return writer.closeAndGetString();
}
@Override
public String toString() {
StringBuilder asset = new StringBuilder();
asset.append("AndroidAppAsset: ");
asset.append(toJson());
return asset.toString();
}
@Override
public boolean equals(Object o) {
if (!(o instanceof AndroidAppAsset)) {
return false;
}
return ((AndroidAppAsset) o).toJson().equals(toJson());
}
@Override
public int hashCode() {
return toJson().hashCode();
}
@Override
public int lookupKey() {
return getPackageName().hashCode();
}
@Override
public boolean followInsecureInclude() {
// Non-HTTPS includes are not allowed in Android App assets.
return false;
}
/**
* Checks that the input is a valid Android app asset.
*
* @param asset a JSONObject that has "namespace", "package_name", and
* "sha256_cert_fingerprints" fields.
* @throws AssociationServiceException if the asset is not well formatted.
*/
public static AndroidAppAsset create(JSONObject asset)
throws AssociationServiceException {
String packageName = asset.optString(Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME);
if (packageName.equals("")) {
throw new AssociationServiceException(String.format(MISSING_FIELD_FORMAT_STRING,
Utils.ANDROID_APP_ASSET_FIELD_PACKAGE_NAME));
}
JSONArray certArray = asset.optJSONArray(Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS);
if (certArray == null || certArray.length() == 0) {
throw new AssociationServiceException(
String.format(MISSING_APPCERTS_FORMAT_STRING,
Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
}
List<String> certFingerprints = new ArrayList<>(certArray.length());
for (int i = 0; i < certArray.length(); i++) {
try {
certFingerprints.add(certArray.getString(i));
} catch (JSONException e) {
throw new AssociationServiceException(
String.format(APPCERT_NOT_STRING_FORMAT_STRING,
Utils.ANDROID_APP_ASSET_FIELD_CERT_FPS));
}
}
return new AndroidAppAsset(packageName, certFingerprints);
}
/**
* Creates a new AndroidAppAsset.
*
* @param packageName the package name of the Android app.
* @param certFingerprints at least one of the Android app signing certificate sha-256
* fingerprint.
*/
public static AndroidAppAsset create(String packageName, List<String> certFingerprints) {
if (packageName == null || packageName.equals("")) {
throw new AssertionError("Expected packageName to be set.");
}
if (certFingerprints == null || certFingerprints.size() == 0) {
throw new AssertionError("Expected certFingerprints to be set.");
}
List<String> lowerFps = new ArrayList<String>(certFingerprints.size());
for (String fp : certFingerprints) {
lowerFps.add(fp.toUpperCase(Locale.US));
}
return new AndroidAppAsset(packageName, lowerFps);
}
private AndroidAppAsset(String packageName, List<String> certFingerprints) {
if (packageName.equals("")) {
mPackageName = null;
} else {
mPackageName = packageName;
}
if (certFingerprints == null || certFingerprints.size() == 0) {
mCertFingerprints = null;
} else {
mCertFingerprints = Collections.unmodifiableList(sortAndDeDuplicate(certFingerprints));
}
}
/**
* Returns an ASCII-sorted copy of the list of certs with all duplicates removed.
*/
private List<String> sortAndDeDuplicate(List<String> certs) {
if (certs.size() <= 1) {
return certs;
}
HashSet<String> set = new HashSet<>(certs);
List<String> result = new ArrayList<>(set);
Collections.sort(result);
return result;
}
}