aboutsummaryrefslogtreecommitdiff
path: root/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java')
-rw-r--r--java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java370
1 files changed, 370 insertions, 0 deletions
diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java b/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java
new file mode 100644
index 0000000..5021317
--- /dev/null
+++ b/java/com/android/libraries/entitlement/eapaka/EapAkaResponse.java
@@ -0,0 +1,370 @@
+/*
+ * 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 com.android.libraries.entitlement.eapaka;
+
+import android.content.Context;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.libraries.entitlement.ServiceEntitlementException;
+import com.android.libraries.entitlement.eapaka.utils.BytesConverter;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import androidx.annotation.Nullable;
+
+/** Generate the response of EAP-AKA token challenge for initial AUTN. */
+class EapAkaResponse {
+ private static final String TAG = "ServiceEntitlement";
+
+ private static final int EAP_AKA_HEADER_LENGTH = 8;
+ private static final byte CODE_REQUEST = 0x01;
+ private static final byte CODE_RESPONSE = 0x02;
+ private static final byte TYPE_EAP_AKA = 0x17;
+ private static final byte SUBTYPE_AKA_CHALLENGE = 0x01;
+ private static final byte ATTRIBUTE_RAND = 0x01;
+ private static final byte ATTRIBUTE_AUTN = 0x02;
+ private static final byte ATTRIBUTE_RES = 0x03;
+ private static final byte ATTRIBUTE_MAC = 0x0B;
+ private static final String ALGORITHM_HMAC_SHA1 = "HmacSHA1";
+ private static final int RAND_LENGTH = 20;
+ private static final int AUTN_LENGTH = 20;
+ private static final int SHA1_OUTPUT_LENGTH = 20;
+
+ /** RAND length 16. */
+ private static final byte RAND_LEN = 0x10;
+ /** AUTN length 16. */
+ private static final byte AUTN_LEN = 0x10;
+
+ /* 1 for Request, 2 for Response*/
+ private byte code = -1;
+ /* The identifier of Response must same as Request */
+ private byte identifier = -1;
+ /* The total length of full EAP-AKA message, include code, identifier, ... */
+ private int length = -1;
+ /* In EAP-AKA, the Type field is set to 23 */
+ private byte type = -1;
+ /* SubType for AKA-Challenge should be 1 */
+ private byte subType = -1;
+ /* The value of AT_AUTN, network authentication token */
+ private byte[] autn;
+ /* The value of AT_RAND, RAND random number*/
+ private byte[] rand;
+
+ private boolean valid;
+
+ public EapAkaResponse(String eapAkaChallenge) {
+ try {
+ parseEapAkaChallengeRequest(eapAkaChallenge);
+ } catch (Exception e) {
+ Log.e(TAG, "parseEapAkaChallengeRequest Exception:", e);
+ valid = false;
+ }
+ }
+
+ /** Refer to RFC 4187 Section 8.1 Message Format/RFC 3748 Session 4 EAP Packet Format. */
+ private void parseEapAkaChallengeRequest(String request) {
+ if (TextUtils.isEmpty(request)) {
+ return;
+ }
+
+ try {
+ byte[] data = Base64.decode(request, Base64.DEFAULT);
+ if (parseEapAkaHeader(data) && parseRandAndAutn(data)) {
+ valid = true;
+ } else {
+ Log.d(TAG, "Invalid data!");
+ }
+ } catch (IllegalArgumentException illegalArgumentException) {
+ Log.e(TAG, "Invalid base-64 content");
+ }
+ }
+
+ /**
+ * Parse EAP-AKA header, 8 bytes include 2 reserved bytes.
+ *
+ * @param data raw bytes of request data.
+ * @return {@code true} if success to parse the header of request data.
+ */
+ private boolean parseEapAkaHeader(byte[] data) {
+ if (data.length <= EAP_AKA_HEADER_LENGTH) {
+ return false;
+ }
+ code = data[0];
+ identifier = data[1];
+ length = ((data[2] & 0xff) << 8) | (data[3] & 0xff);
+ type = data[4];
+ subType = data[5];
+
+ // valid header
+ if (code != CODE_REQUEST
+ || length != data.length
+ || type != TYPE_EAP_AKA
+ || subType != SUBTYPE_AKA_CHALLENGE) {
+ Log.d(
+ TAG,
+ "Invalid EAP-AKA Header, code="
+ + code
+ + ", length="
+ + length
+ + ", real length="
+ + data.length
+ + ", type="
+ + type
+ + ", subType="
+ + subType);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Refer to RFC 4187 section 10.6 AT_RAND/RFC 4187 section 10.7 AT_AUTN.
+ *
+ * @param data raw bytes of request data.
+ * @return {@code true} if success to parse the RAND and AUTN data.
+ */
+ private boolean parseRandAndAutn(byte[] data) {
+ int index = EAP_AKA_HEADER_LENGTH;
+ while (index < data.length) {
+ int remainsLength = data.length - index;
+ if (remainsLength <= 2) {
+ Log.d(TAG, "Error! remainsLength = " + remainsLength);
+ return false;
+ }
+
+ byte attributeType = data[index];
+
+ // the length of this attribute in multiples of 4 bytes, include attribute type and
+ // length
+ int length = (data[index + 1] & 0xff) * 4;
+ if (length > remainsLength) {
+ Log.d(TAG,
+ "Length Error! length is " + length + " but only remains " + remainsLength);
+ return false;
+ }
+
+ // see RFC 4187 section 11 for attribute type
+ if (attributeType == ATTRIBUTE_RAND) {
+ if (length != RAND_LENGTH) {
+ Log.d(TAG, "AT_RAND length is " + length);
+ return false;
+ }
+ rand = new byte[16];
+ System.arraycopy(data, index + 4, rand, 0, 16);
+ } else if (attributeType == ATTRIBUTE_AUTN) {
+ if (length != AUTN_LENGTH) {
+ Log.d(TAG, "AT_AUTN length is " + length);
+ return false;
+ }
+ autn = new byte[16];
+ System.arraycopy(data, index + 4, autn, 0, 16);
+ }
+
+ index += length;
+ } // while
+
+ // check has AT_RAND and AT_AUTH
+ if (rand == null || autn == null) {
+ Log.d(TAG, "Invalid Type Datas!");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns EAP-AKA challenge response message which generated with SIM EAP-AKA authentication
+ * with
+ * network provided EAP-AKA challenge request message.
+ */
+ public String getEapAkaChallengeResponse(Context context, int simSubscriptionId)
+ throws ServiceEntitlementException {
+ if (!valid) {
+ throw new ServiceEntitlementException("EAP-AKA Challenge message not valid!");
+ }
+
+ TelephonyManager telephonyManager =
+ context.getSystemService(TelephonyManager.class).createForSubscriptionId(
+ simSubscriptionId);
+
+ // process EAP-AKA authentication with SIM
+ String response =
+ telephonyManager.getIccAuthentication(
+ TelephonyManager.APPTYPE_USIM,
+ TelephonyManager.AUTHTYPE_EAP_AKA,
+ getSimAuthChallengeData());
+
+ EapAkaSecurityContext securityContext = EapAkaSecurityContext.from(response);
+ // RFC 4187, section 7. Key Generation
+ // generate master key
+ MasterKey mk =
+ MasterKey.create(
+ EapAkaApi.getImsiEap(telephonyManager.getSubscriberId(),
+ telephonyManager.getSimOperator()),
+ securityContext.getIk(),
+ securityContext.getCk());
+ // K_aut is the key used to calculate MAC
+ if (mk.getAut() == null) {
+ throw new ServiceEntitlementException("Can't generate K_Aut!");
+ }
+
+ // generate EAP-AKA Challenge Response message
+ byte[] challengeResponse =
+ generateEapAkaChallengeResponse(securityContext.getRes(), mk.getAut());
+ if (challengeResponse == null) {
+ throw new ServiceEntitlementException(
+ "Failed to generate EAP-AKA Challenge Response data!");
+ }
+
+ return Base64.encodeToString(challengeResponse, Base64.NO_WRAP).trim();
+ }
+
+ /** Returns Base64 encoded GSM/3G security context for SIM Authentication request. */
+ @Nullable
+ private String getSimAuthChallengeData() {
+ if (!valid) {
+ return null;
+ }
+
+ byte[] challengeData = new byte[RAND_LEN + AUTN_LEN + 2];
+ challengeData[0] = RAND_LEN;
+ System.arraycopy(rand, 0, challengeData, 1, RAND_LEN);
+ challengeData[RAND_LEN + 1] = AUTN_LEN;
+ System.arraycopy(autn, 0, challengeData, RAND_LEN + 2, AUTN_LEN);
+
+ return Base64.encodeToString(challengeData, Base64.NO_WRAP).trim();
+ }
+
+ /** Returns EAP-AKA Challenge response message byte array data or null if failed to generate. */
+ @Nullable
+ public byte[] generateEapAkaChallengeResponse(@Nullable byte[] res, byte[] aut) {
+ if (res == null || aut == null) {
+ return null;
+ }
+
+ byte[] message = createEapAkaChallengeResponse(res);
+
+ // use K_aut as key to calculate mac
+ byte[] mac = calculateMac(aut, message);
+ if (mac == null) {
+ return null;
+ }
+
+ // fill MAC value to the message
+ // The value start index is 8 + AT_RES (4 + res.length) + header of AT_MAC (4)
+ int index = 8 + 4 + res.length + 4;
+ System.arraycopy(mac, 0, message, index, mac.length);
+
+ return message;
+ }
+
+ // AT_MAC/AT_RES are must included in response message
+ //
+ // Reference RFC 4187 Section 8.1 Message Format
+ // RFC 4187 Section 9.4 EAP-Response/AKA-Challenge
+ // RFC 3748, Section 4.1. Request and Response
+ private byte[] createEapAkaChallengeResponse(byte[] res) {
+ // size = 8 (header) + resHeader (4) + res.length + AT_MAC (20 bytes)
+ byte[] message = new byte[32 + res.length];
+
+ // set up header
+ message[0] = CODE_RESPONSE;
+ // Identifier need to same with request
+ message[1] = identifier;
+ // length include entire EAP-AKA message
+ byte[] lengthBytes = BytesConverter.convertIntegerTo4Bytes(message.length);
+ message[2] = lengthBytes[2];
+ message[3] = lengthBytes[3];
+ message[4] = TYPE_EAP_AKA;
+ message[5] = SUBTYPE_AKA_CHALLENGE;
+ // Reserved 2 bytes
+ message[6] = 0x00;
+ message[7] = 0x00;
+
+ int index = 8;
+
+ // set up AT_RES, RFC 4187, Section 10.8 AT_RES
+ message[index++] = ATTRIBUTE_RES;
+ // The length of the AT_RES attribute must be a multiple of 4 bytes which identifies the
+ // exact length of the RES in bits. To pad 4 onto the length to ensure the reserved buffer
+ // size large enough after convert to byte count.
+ int resLength = (res.length + 4) / 4;
+ message[index++] = (byte) (resLength & 0xff);
+ // The value field of this attribute begins with the 2-byte RES Length, which identifies
+ // the exact length of the RES in bits.
+ byte[] resBitLength = BytesConverter.convertIntegerTo4Bytes(res.length * 8);
+ message[index++] = resBitLength[2];
+ message[index++] = resBitLength[3];
+ System.arraycopy(res, 0, message, index, res.length);
+ index += res.length;
+
+ // set up AT_MAC, RFC 4187, 10.15 AT_MAC
+ message[index++] = ATTRIBUTE_MAC;
+ // fixed length, 5*4 = 20
+ message[index++] = 0x05;
+ // With two bytes reserved
+ message[index++] = 0x00;
+ message[index++] = 0x00;
+
+ // The MAC is calculated over the whole EAP packet and concatenated with optional
+ // message-specific data, with the exception that the value field of the
+ // MAC attribute is set to zero when calculating the MAC.
+ for (int i = 0; i < 16; i++) {
+ message[index++] = 0x00;
+ }
+
+ return message;
+ }
+
+ // See RFC 4187, 10.15 AT_MAC, snippet as below, the key must be k_aut
+ //
+ // The MAC algorithm is HMAC-SHA1-128 [RFC2104] keyed hash value. (The
+ // HMAC-SHA1-128 value is obtained from the 20-byte HMAC-SHA1 value by
+ // truncating the output to 16 bytes. Hence, the length of the MAC is
+ // 16 bytes.) The derivation of the authentication key (K_aut) used in
+ // the calculation of the MAC is specified in Section 7.
+ @Nullable
+ private byte[] calculateMac(byte[] key, byte[] message) {
+ try {
+ Mac mac = Mac.getInstance(ALGORITHM_HMAC_SHA1);
+ SecretKeySpec secret = new SecretKeySpec(key, ALGORITHM_HMAC_SHA1);
+ mac.init(secret);
+ byte[] output = mac.doFinal(message);
+
+ if (output == null || output.length != SHA1_OUTPUT_LENGTH) {
+ Log.e(TAG, "Invalid result! length should be 20, but " + output.length);
+ return null;
+ }
+
+ byte[] macValue = new byte[16];
+ System.arraycopy(output, 0, macValue, 0, 16);
+ return macValue;
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ Log.e(TAG, "calculateMac failed!", e);
+ }
+
+ return null;
+ }
+}