blob: 9d6786b4ac4eed7187a9bbcd82ad647f6888c05e [file] [log] [blame]
/*
* Copyright (C) 2021 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 android.devicepolicy.cts;
import static android.Manifest.permission.LOCAL_MAC_ADDRESS;
import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE;
import static com.google.common.truth.Truth.assertThat;
import static org.testng.Assert.assertThrows;
import android.annotation.NonNull;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.telephony.TelephonyManager;
import com.android.bedstead.harrier.BedsteadJUnit4;
import com.android.bedstead.harrier.DeviceState;
import com.android.bedstead.harrier.annotations.EnsureHasPermission;
import com.android.bedstead.harrier.annotations.Postsubmit;
import com.android.bedstead.harrier.annotations.enterprise.PolicyAppliesTest;
import com.android.bedstead.harrier.policies.EnrollmentSpecificId;
import com.android.bedstead.nene.TestApis;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.runner.RunWith;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@RunWith(BedsteadJUnit4.class)
public final class EnrollmentSpecificIdTest {
private static final String ORGANIZATION_ID = "abcxyz123";
private static final String DIFFERENT_ORGANIZATION_ID = "xyz";
@ClassRule
@Rule
public static final DeviceState sDeviceState = new DeviceState();
private static final Context sContext = TestApis.context().instrumentedContext();
@Postsubmit(reason = "New test")
@PolicyAppliesTest(policy = EnrollmentSpecificId.class)
public void emptyOrganizationId_throws() {
assertThrows(IllegalArgumentException.class,
() -> sDeviceState.dpc().devicePolicyManager().setOrganizationId(""));
}
@Postsubmit(reason = "New test")
@PolicyAppliesTest(policy = EnrollmentSpecificId.class)
public void reSetOrganizationId_throws() {
try {
sDeviceState.dpc().devicePolicyManager().setOrganizationId(ORGANIZATION_ID);
assertThrows(IllegalStateException.class,
() -> sDeviceState.dpc().devicePolicyManager()
.setOrganizationId(DIFFERENT_ORGANIZATION_ID));
} finally {
TestApis.devicePolicy().clearOrganizationId(sDeviceState.dpc().user());
}
}
/**
* This test tests that the platform calculates the ESID according to the specification and
* does not, for example, return the same ESID regardless of the managing package.
*/
@Postsubmit(reason = "New test")
@PolicyAppliesTest(policy = EnrollmentSpecificId.class)
@EnsureHasPermission({READ_PRIVILEGED_PHONE_STATE, NETWORK_SETTINGS, LOCAL_MAC_ADDRESS})
public void enrollmentSpecificId_CorrectlyCalculated() {
try {
sDeviceState.dpc().devicePolicyManager().setOrganizationId(ORGANIZATION_ID);
final String esidFromDpm = sDeviceState.dpc().devicePolicyManager()
.getEnrollmentSpecificId();
final String calculatedEsid = calculateEsid(
sDeviceState.dpc().componentName().getPackageName(),
ORGANIZATION_ID);
assertThat(esidFromDpm).isEqualTo(calculatedEsid);
} finally {
TestApis.devicePolicy().clearOrganizationId(sDeviceState.dpc().user());
}
}
private String calculateEsid(String profileOwnerPackage, String enterpriseIdString) {
TelephonyManager telephonyService = sContext.getSystemService(TelephonyManager.class);
assertThat(telephonyService).isNotNull();
WifiManager wifiManager = sContext.getSystemService(WifiManager.class);
assertThat(wifiManager).isNotNull();
final byte[] serialNumber = getPaddedHardwareIdentifier(Build.getSerial()).getBytes();
final byte[] imei = getPaddedHardwareIdentifier(telephonyService.getImei(0)).getBytes();
final byte[] meid = getPaddedHardwareIdentifier(telephonyService.getMeid(0)).getBytes();
final byte[] macAddress;
final String[] macAddresses = wifiManager.getFactoryMacAddresses();
if (macAddresses == null || macAddresses.length == 0) {
macAddress = "".getBytes();
} else {
macAddress = macAddresses[0].getBytes();
}
final int totalIdentifiersLength = serialNumber.length + imei.length + meid.length
+ macAddress.length;
final ByteBuffer fixedIdentifiers = ByteBuffer.allocate(totalIdentifiersLength);
fixedIdentifiers.put(serialNumber);
fixedIdentifiers.put(imei);
fixedIdentifiers.put(meid);
fixedIdentifiers.put(macAddress);
final byte[] dpcPackage = getPaddedProfileOwnerName(profileOwnerPackage).getBytes();
final byte[] enterpriseId = getPaddedEnterpriseId(enterpriseIdString).getBytes();
final ByteBuffer info = ByteBuffer.allocate(dpcPackage.length + enterpriseId.length);
info.put(dpcPackage);
info.put(enterpriseId);
final byte[] esidBytes = computeHkdf("HMACSHA256", fixedIdentifiers.array(), null,
info.array(), 16);
ByteBuffer esidByteBuffer = ByteBuffer.wrap(esidBytes);
return encodeBase32(esidByteBuffer.getLong()) + encodeBase32(esidByteBuffer.getLong());
}
private static String getPaddedHardwareIdentifier(String hardwareIdentifier) {
if (hardwareIdentifier == null) {
hardwareIdentifier = "";
}
String hwIdentifier = String.format("%16s", hardwareIdentifier);
return hwIdentifier.substring(0, 16);
}
private static String getPaddedProfileOwnerName(String profileOwnerPackage) {
return String.format("%64s", profileOwnerPackage);
}
private static String getPaddedEnterpriseId(String enterpriseId) {
return String.format("%64s", enterpriseId);
}
// Copied from android.security.identity.Util, here to make sure Enterprise-Specific ID is
// calculated according to spec.
@NonNull
private static byte[] computeHkdf(
@NonNull String macAlgorithm, @NonNull final byte[] ikm, @NonNull final byte[] salt,
@NonNull final byte[] info, int size) {
Mac mac = null;
try {
mac = Mac.getInstance(macAlgorithm);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("No such algorithm: " + macAlgorithm, e);
}
if (size > 255 * mac.getMacLength()) {
throw new RuntimeException("size too large");
}
try {
if (salt == null || salt.length == 0) {
// According to RFC 5869, Section 2.2 the salt is optional. If no salt is provided
// then HKDF uses a salt that is an array of zeros of the same length as the hash
// digest.
mac.init(new SecretKeySpec(new byte[mac.getMacLength()], macAlgorithm));
} else {
mac.init(new SecretKeySpec(salt, macAlgorithm));
}
byte[] prk = mac.doFinal(ikm);
byte[] result = new byte[size];
int ctr = 1;
int pos = 0;
mac.init(new SecretKeySpec(prk, macAlgorithm));
byte[] digest = new byte[0];
while (true) {
mac.update(digest);
mac.update(info);
mac.update((byte) ctr);
digest = mac.doFinal();
if (pos + digest.length < size) {
System.arraycopy(digest, 0, result, pos, digest.length);
pos += digest.length;
ctr++;
} else {
System.arraycopy(digest, 0, result, pos, size - pos);
break;
}
}
return result;
} catch (InvalidKeyException e) {
throw new RuntimeException("Error MACing", e);
}
}
private static final char[] ENCODE = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '2', '3', '4', '5', '6', '7',
};
private static final char SEPARATOR = '-';
private static final int LONG_SIZE = 13;
private static final int GROUP_SIZE = 4;
private static String encodeBase32(long input) {
final char[] alphabet = ENCODE;
/*
* Make a character array with room for the separators between each
* group.
*/
final char[] encoded = new char[LONG_SIZE + (LONG_SIZE / GROUP_SIZE)];
int index = encoded.length;
for (int i = 0; i < LONG_SIZE; i++) {
/*
* Make sure we don't put a separator at the beginning. Since we're
* building from the rear of the array, we use (LONG_SIZE %
* GROUP_SIZE) to make the odd-size group appear at the end instead
* of the beginning.
*/
if (i > 0 && (i % GROUP_SIZE) == (LONG_SIZE % GROUP_SIZE)) {
encoded[--index] = SEPARATOR;
}
/*
* Extract 5 bits of data, then shift it out.
*/
final int group = (int) (input & 0x1F);
input >>>= 5;
encoded[--index] = alphabet[group];
}
return String.valueOf(encoded);
}
}