content: /*
   * Copyright (C) 2018 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.server.locksettings.recoverablekeystore.serialization;
  
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.CERTIFICATE_FACTORY_TYPE;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.NAMESPACE;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.OUTPUT_ENCODING;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_ALGORITHM;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_ALIAS;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_APPLICATION_KEY;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_APPLICATION_KEYS;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_BACKEND_PUBLIC_KEY;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_COUNTER_ID;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_CHAIN_PROTECTION_PARAMS;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_CHAIN_SNAPSHOT;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_DERIVATION_PARAMS;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_MATERIAL;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_KEY_METADATA;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_LOCK_SCREEN_UI_TYPE;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_MAX_ATTEMPTS;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_MEMORY_DIFFICULTY;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_RECOVERY_KEY_MATERIAL;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_SALT;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_SERVER_PARAMS;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_SNAPSHOT_VERSION;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_TRUSTED_HARDWARE_CERT_PATH;
  import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_USER_SECRET_TYPE;
  
  import android.security.keystore.recovery.KeyChainProtectionParams;
  import android.security.keystore.recovery.KeyChainSnapshot;
  import android.security.keystore.recovery.KeyDerivationParams;
  import android.security.keystore.recovery.WrappedApplicationKey;
  import android.util.Base64;
  import android.util.Xml;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.ByteArrayInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.security.cert.CertPath;
  import java.security.cert.CertificateException;
  import java.security.cert.CertificateFactory;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Locale;
  
  /**
   * Deserializes a {@link android.security.keystore.recovery.KeyChainSnapshot} instance from XML.
   */
  public class KeyChainSnapshotDeserializer {
  
      /**
       * Deserializes a {@link KeyChainSnapshot} instance from the XML in the {@code inputStream}.
       *
       * @throws IOException if there is an IO error reading from the stream.
       * @throws KeyChainSnapshotParserException if the XML does not conform to the expected XML for
       *     a snapshot.
       */
      public static KeyChainSnapshot deserialize(InputStream inputStream)
              throws KeyChainSnapshotParserException, IOException {
          try {
              return deserializeInternal(inputStream);
          } catch (XmlPullParserException e) {
              throw new KeyChainSnapshotParserException("Malformed KeyChainSnapshot XML", e);
          }
      }
  
      private static KeyChainSnapshot deserializeInternal(InputStream inputStream) throws IOException,
              XmlPullParserException, KeyChainSnapshotParserException {
          XmlPullParser parser = Xml.newPullParser();
          parser.setInput(inputStream, OUTPUT_ENCODING);
  
          parser.nextTag();
          parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_KEY_CHAIN_SNAPSHOT);
  
          KeyChainSnapshot.Builder builder = new KeyChainSnapshot.Builder();
          while (parser.next() != XmlPullParser.END_TAG) {
              if (parser.getEventType() != XmlPullParser.START_TAG) {
                  continue;
              }
  
              String name = parser.getName();
  
              switch (name) {
                  case TAG_SNAPSHOT_VERSION:
                      builder.setSnapshotVersion(readIntTag(parser, TAG_SNAPSHOT_VERSION));
                      break;
  
                  case TAG_RECOVERY_KEY_MATERIAL:
                      builder.setEncryptedRecoveryKeyBlob(
                              readBlobTag(parser, TAG_RECOVERY_KEY_MATERIAL));
                      break;
  
                  case TAG_COUNTER_ID:
                      builder.setCounterId(readLongTag(parser, TAG_COUNTER_ID));
                      break;
  
                  case TAG_SERVER_PARAMS:
                      builder.setServerParams(readBlobTag(parser, TAG_SERVER_PARAMS));
                      break;
  
                  case TAG_MAX_ATTEMPTS:
                      builder.setMaxAttempts(readIntTag(parser, TAG_MAX_ATTEMPTS));
                      break;
  
                  case TAG_TRUSTED_HARDWARE_CERT_PATH:
                      try {
                          builder.setTrustedHardwareCertPath(
                                  readCertPathTag(parser, TAG_TRUSTED_HARDWARE_CERT_PATH));
                      } catch (CertificateException e) {
                          throw new KeyChainSnapshotParserException(
                                  "Could not set trustedHardwareCertPath", e);
                      }
                      break;
  
                  case TAG_BACKEND_PUBLIC_KEY:
                      // Unused
                      break;
  
                  case TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST:
                      builder.setKeyChainProtectionParams(readKeyChainProtectionParamsList(parser));
                      break;
  
                  case TAG_APPLICATION_KEYS:
                      builder.setWrappedApplicationKeys(readWrappedApplicationKeys(parser));
                      break;
  
                  default:
                      throw new KeyChainSnapshotParserException(String.format(
                              Locale.US, "Unexpected tag %s in keyChainSnapshot", name));
              }
          }
  
          parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_KEY_CHAIN_SNAPSHOT);
          try {
              return builder.build();
          } catch (NullPointerException e) {
              throw new KeyChainSnapshotParserException("Failed to build KeyChainSnapshot", e);
          }
      }
  
      private static List<WrappedApplicationKey> readWrappedApplicationKeys(XmlPullParser parser)
              throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
          parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_APPLICATION_KEYS);
          ArrayList<WrappedApplicationKey> keys = new ArrayList<>();
          while (parser.next() != XmlPullParser.END_TAG) {
              if (parser.getEventType() != XmlPullParser.START_TAG) {
                  continue;
              }
              keys.add(readWrappedApplicationKey(parser));
          }
          parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_APPLICATION_KEYS);
          return keys;
      }
  
      private static WrappedApplicationKey readWrappedApplicationKey(XmlPullParser parser)
              throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
          parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_APPLICATION_KEY);
          WrappedApplicationKey.Builder builder = new WrappedApplicationKey.Builder();
          while (parser.next() != XmlPullParser.END_TAG) {
              if (parser.getEventType() != XmlPullParser.START_TAG) {
                  continue;
              }
  
              String name = parser.getName();
  
              switch (name) {
                  case TAG_ALIAS:
                      builder.setAlias(readStringTag(parser, TAG_ALIAS));
                      break;
  
                  case TAG_KEY_MATERIAL:
                      builder.setEncryptedKeyMaterial(readBlobTag(parser, TAG_KEY_MATERIAL));
                      break;
  
                  case TAG_KEY_METADATA:
                      builder.setMetadata(readBlobTag(parser, TAG_KEY_METADATA));
                      break;
  
                  default:
                      throw new KeyChainSnapshotParserException(String.format(
                              Locale.US, "Unexpected tag %s in wrappedApplicationKey", name));
              }
          }
          parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_APPLICATION_KEY);
  
          try {
              return builder.build();
          } catch (NullPointerException e) {
              throw new KeyChainSnapshotParserException("Failed to build WrappedApplicationKey", e);
          }
      }
  
      private static List<KeyChainProtectionParams> readKeyChainProtectionParamsList(
              XmlPullParser parser) throws IOException, XmlPullParserException,
              KeyChainSnapshotParserException {
          parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST);
  
          ArrayList<KeyChainProtectionParams> keyChainProtectionParamsList = new ArrayList<>();
          while (parser.next() != XmlPullParser.END_TAG) {
              if (parser.getEventType() != XmlPullParser.START_TAG) {
                  continue;
              }
              keyChainProtectionParamsList.add(readKeyChainProtectionParams(parser));
          }
  
          parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST);
          return keyChainProtectionParamsList;
      }
  
      private static KeyChainProtectionParams readKeyChainProtectionParams(XmlPullParser parser)
          throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
          parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS);
  
          KeyChainProtectionParams.Builder builder = new KeyChainProtectionParams.Builder();
          while (parser.next() != XmlPullParser.END_TAG) {
              if (parser.getEventType() != XmlPullParser.START_TAG) {
                  continue;
              }
  
              String name = parser.getName();
  
              switch (name) {
                  case TAG_LOCK_SCREEN_UI_TYPE:
                      builder.setLockScreenUiFormat(readIntTag(parser, TAG_LOCK_SCREEN_UI_TYPE));
                      break;
  
                  case TAG_USER_SECRET_TYPE:
                      builder.setUserSecretType(readIntTag(parser, TAG_USER_SECRET_TYPE));
                      break;
  
                  case TAG_KEY_DERIVATION_PARAMS:
                      builder.setKeyDerivationParams(readKeyDerivationParams(parser));
                      break;
  
                  default:
                      throw new KeyChainSnapshotParserException(String.format(
                              Locale.US,
                              "Unexpected tag %s in keyChainProtectionParams",
                              name));
  
              }
          }
  
          parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS);
  
          try {
              return builder.build();
          } catch (NullPointerException e) {
              throw new KeyChainSnapshotParserException(
                      "Failed to build KeyChainProtectionParams", e);
          }
      }
  
      private static KeyDerivationParams readKeyDerivationParams(XmlPullParser parser)
              throws XmlPullParserException, IOException, KeyChainSnapshotParserException {
          parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_KEY_DERIVATION_PARAMS);
  
          int memoryDifficulty = -1;
          int algorithm = -1;
          byte[] salt = null;
  
          while (parser.next() != XmlPullParser.END_TAG) {
              if (parser.getEventType() != XmlPullParser.START_TAG) {
                  continue;
              }
  
              String name = parser.getName();
  
              switch (name) {
                  case TAG_MEMORY_DIFFICULTY:
                      memoryDifficulty = readIntTag(parser, TAG_MEMORY_DIFFICULTY);
                      break;
  
                  case TAG_ALGORITHM:
                      algorithm = readIntTag(parser, TAG_ALGORITHM);
                      break;
  
                  case TAG_SALT:
                      salt = readBlobTag(parser, TAG_SALT);
                      break;
  
                  default:
                      throw new KeyChainSnapshotParserException(
                              String.format(
                                      Locale.US,
                                      "Unexpected tag %s in keyDerivationParams",
                                      name));
              }
          }
  
          if (salt == null) {
              throw new KeyChainSnapshotParserException("salt was not set in keyDerivationParams");
          }
  
          KeyDerivationParams keyDerivationParams = null;
  
          switch (algorithm) {
              case KeyDerivationParams.ALGORITHM_SHA256:
                  keyDerivationParams = KeyDerivationParams.createSha256Params(salt);
                  break;
  
              case KeyDerivationParams.ALGORITHM_SCRYPT:
                  keyDerivationParams = KeyDerivationParams.createScryptParams(
                          salt, memoryDifficulty);
                  break;
  
              default:
                  throw new KeyChainSnapshotParserException(
                          "Unknown algorithm in keyDerivationParams");
          }
  
          parser.require(XmlPullParser.END_TAG, NAMESPACE, TAG_KEY_DERIVATION_PARAMS);
          return keyDerivationParams;
      }
  
      private static int readIntTag(XmlPullParser parser, String tagName)
              throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
          parser.require(XmlPullParser.START_TAG, NAMESPACE, tagName);
          String text = readText(parser);
          parser.require(XmlPullParser.END_TAG, NAMESPACE, tagName);
          try {
              return Integer.valueOf(text);
          } catch (NumberFormatException e) {
              throw new KeyChainSnapshotParserException(
                      String.format(
                              Locale.US, "%s expected int but got '%s'", tagName, text), e);
          }
      }
  
      private static long readLongTag(XmlPullParser parser, String tagName)
              throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
          parser.require(XmlPullParser.START_TAG, NAMESPACE, tagName);
          String text = readText(parser);
          parser.require(XmlPullParser.END_TAG, NAMESPACE, tagName);
          try {
              return Long.valueOf(text);
          } catch (NumberFormatException e) {
              throw new KeyChainSnapshotParserException(
                      String.format(
                              Locale.US, "%s expected long but got '%s'", tagName, text), e);
          }
      }
  
      private static String readStringTag(XmlPullParser parser, String tagName)
              throws IOException, XmlPullParserException {
          parser.require(XmlPullParser.START_TAG, NAMESPACE, tagName);
          String text = readText(parser);
          parser.require(XmlPullParser.END_TAG, NAMESPACE, tagName);
          return text;
      }
  
      private static byte[] readBlobTag(XmlPullParser parser, String tagName)
              throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
          parser.require(XmlPullParser.START_TAG, NAMESPACE, tagName);
          String text = readText(parser);
          parser.require(XmlPullParser.END_TAG, NAMESPACE, tagName);
  
          try {
              return Base64.decode(text, /*flags=*/ Base64.DEFAULT);
          } catch (IllegalArgumentException e) {
              throw new KeyChainSnapshotParserException(
                      String.format(
                              Locale.US,
                              "%s expected base64 encoded bytes but got '%s'",
                              tagName, text), e);
          }
      }
  
      private static CertPath readCertPathTag(XmlPullParser parser, String tagName)
              throws IOException, XmlPullParserException, KeyChainSnapshotParserException {
          byte[] bytes = readBlobTag(parser, tagName);
          try {
              return CertificateFactory.getInstance(CERTIFICATE_FACTORY_TYPE)
                      .generateCertPath(new ByteArrayInputStream(bytes));
          } catch (CertificateException e) {
              throw new KeyChainSnapshotParserException("Could not parse CertPath in tag " + tagName,
                      e);
          }
      }
  
      private static String readText(XmlPullParser parser)
              throws IOException, XmlPullParserException {
          String result = "";
          if (parser.next() == XmlPullParser.TEXT) {
              result = parser.getText();
              parser.nextTag();
          }
          return result;
      }
  
      // Statics only
      private KeyChainSnapshotDeserializer() {}
  }
  