diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java new file mode 100644 index 0000000000000..dcaa0b4785cc4 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotDeserializer.java @@ -0,0 +1,401 @@ +/* + * 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_COUNTER_ID; +import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_RECOVERY_KEY_MATERIAL; +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_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_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 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; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * 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_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 readWrappedApplicationKeys(XmlPullParser parser) + throws IOException, XmlPullParserException, KeyChainSnapshotParserException { + parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_APPLICATION_KEYS); + ArrayList 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; + + 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 readKeyChainProtectionParamsList( + XmlPullParser parser) throws IOException, XmlPullParserException, + KeyChainSnapshotParserException { + parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST); + + ArrayList 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() {} +} diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotParserException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotParserException.java new file mode 100644 index 0000000000000..a3208afddf9b4 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotParserException.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * Error thrown when parsing invalid XML, while trying to read a + * {@link android.security.keystore.recovery.KeyChainSnapshot}. + */ +public class KeyChainSnapshotParserException extends Exception { + + public KeyChainSnapshotParserException(String message, Throwable cause) { + super(message, cause); + } + + public KeyChainSnapshotParserException(String message) { + super(message); + } +} diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSchema.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSchema.java new file mode 100644 index 0000000000000..ee8b2cfd18f54 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSchema.java @@ -0,0 +1,57 @@ +/* + * 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; + +/** + * Describes the XML schema of the {@link android.security.keystore.recovery.KeyChainSnapshot} file. + */ +class KeyChainSnapshotSchema { + static final String NAMESPACE = null; + + static final String OUTPUT_ENCODING = "UTF-8"; + + static final String CERTIFICATE_FACTORY_TYPE = "X.509"; + static final String CERT_PATH_ENCODING = "PkiPath"; + + static final String TAG_KEY_CHAIN_SNAPSHOT = "keyChainSnapshot"; + + static final String TAG_SNAPSHOT_VERSION = "snapshotVersion"; + static final String TAG_COUNTER_ID = "counterId"; + static final String TAG_MAX_ATTEMPTS = "maxAttempts"; + static final String TAG_RECOVERY_KEY_MATERIAL = "recoveryKeyMaterial"; + static final String TAG_SERVER_PARAMS = "serverParams"; + static final String TAG_TRUSTED_HARDWARE_CERT_PATH = "thmCertPath"; + + static final String TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST = + "keyChainProtectionParamsList"; + static final String TAG_KEY_CHAIN_PROTECTION_PARAMS = "keyChainProtectionParams"; + static final String TAG_USER_SECRET_TYPE = "userSecretType"; + static final String TAG_LOCK_SCREEN_UI_TYPE = "lockScreenUiType"; + + static final String TAG_KEY_DERIVATION_PARAMS = "keyDerivationParams"; + static final String TAG_ALGORITHM = "algorithm"; + static final String TAG_MEMORY_DIFFICULTY = "memoryDifficulty"; + static final String TAG_SALT = "salt"; + + static final String TAG_APPLICATION_KEYS = "applicationKeysList"; + static final String TAG_APPLICATION_KEY = "applicationKey"; + static final String TAG_ALIAS = "alias"; + static final String TAG_KEY_MATERIAL = "keyMaterial"; + + // Statics only + private KeyChainSnapshotSchema() {} +} diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java new file mode 100644 index 0000000000000..f817a8fb02e4a --- /dev/null +++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializer.java @@ -0,0 +1,196 @@ +/* + * 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.CERT_PATH_ENCODING; +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_COUNTER_ID; +import static com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSchema.TAG_RECOVERY_KEY_MATERIAL; +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_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_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.XmlSerializer; + +import java.io.IOException; +import java.io.OutputStream; +import java.security.cert.CertPath; +import java.security.cert.CertificateEncodingException; +import java.util.List; + +/** + * Serializes a {@link KeyChainSnapshot} instance to XML. + */ +public class KeyChainSnapshotSerializer { + + /** + * Serializes {@code keyChainSnapshot} to XML, writing to {@code outputStream}. + * + * @throws IOException if there was an IO error writing to the stream. + * @throws CertificateEncodingException if the {@link CertPath} from + * {@link KeyChainSnapshot#getTrustedHardwareCertPath()} is not encoded correctly. + */ + public static void serialize(KeyChainSnapshot keyChainSnapshot, OutputStream outputStream) + throws IOException, CertificateEncodingException { + XmlSerializer xmlSerializer = Xml.newSerializer(); + xmlSerializer.setOutput(outputStream, OUTPUT_ENCODING); + xmlSerializer.startDocument( + /*encoding=*/ null, + /*standalone=*/ null); + xmlSerializer.startTag(NAMESPACE, TAG_KEY_CHAIN_SNAPSHOT); + writeKeyChainSnapshotProperties(xmlSerializer, keyChainSnapshot); + writeKeyChainProtectionParams(xmlSerializer, + keyChainSnapshot.getKeyChainProtectionParams()); + writeApplicationKeys(xmlSerializer, + keyChainSnapshot.getWrappedApplicationKeys()); + xmlSerializer.endTag(NAMESPACE, TAG_KEY_CHAIN_SNAPSHOT); + xmlSerializer.endDocument(); + } + + private static void writeApplicationKeys( + XmlSerializer xmlSerializer, List wrappedApplicationKeys) + throws IOException { + xmlSerializer.startTag(NAMESPACE, TAG_APPLICATION_KEYS); + for (WrappedApplicationKey key : wrappedApplicationKeys) { + xmlSerializer.startTag(NAMESPACE, TAG_APPLICATION_KEY); + writeApplicationKeyProperties(xmlSerializer, key); + xmlSerializer.endTag(NAMESPACE, TAG_APPLICATION_KEY); + } + xmlSerializer.endTag(NAMESPACE, TAG_APPLICATION_KEYS); + } + + private static void writeApplicationKeyProperties( + XmlSerializer xmlSerializer, WrappedApplicationKey applicationKey) throws IOException { + writePropertyTag(xmlSerializer, TAG_ALIAS, applicationKey.getAlias()); + writePropertyTag(xmlSerializer, TAG_KEY_MATERIAL, applicationKey.getEncryptedKeyMaterial()); + } + + private static void writeKeyChainProtectionParams( + XmlSerializer xmlSerializer, + List keyChainProtectionParamsList) throws IOException { + xmlSerializer.startTag(NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST); + for (KeyChainProtectionParams keyChainProtectionParams : keyChainProtectionParamsList) { + xmlSerializer.startTag(NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS); + writeKeyChainProtectionParamsProperties(xmlSerializer, keyChainProtectionParams); + xmlSerializer.endTag(NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS); + } + xmlSerializer.endTag(NAMESPACE, TAG_KEY_CHAIN_PROTECTION_PARAMS_LIST); + } + + private static void writeKeyChainProtectionParamsProperties( + XmlSerializer xmlSerializer, KeyChainProtectionParams keyChainProtectionParams) + throws IOException { + writePropertyTag(xmlSerializer, TAG_USER_SECRET_TYPE, + keyChainProtectionParams.getUserSecretType()); + writePropertyTag(xmlSerializer, TAG_LOCK_SCREEN_UI_TYPE, + keyChainProtectionParams.getLockScreenUiFormat()); + + // NOTE: Do not serialize the 'secret' field. It should never be set anyway for snapshots + // we generate. + + writeKeyDerivationParams(xmlSerializer, keyChainProtectionParams.getKeyDerivationParams()); + } + + private static void writeKeyDerivationParams( + XmlSerializer xmlSerializer, KeyDerivationParams keyDerivationParams) + throws IOException { + xmlSerializer.startTag(NAMESPACE, TAG_KEY_DERIVATION_PARAMS); + writeKeyDerivationParamsProperties( + xmlSerializer, keyDerivationParams); + xmlSerializer.endTag(NAMESPACE, TAG_KEY_DERIVATION_PARAMS); + } + + private static void writeKeyDerivationParamsProperties( + XmlSerializer xmlSerializer, KeyDerivationParams keyDerivationParams) + throws IOException { + writePropertyTag(xmlSerializer, TAG_ALGORITHM, keyDerivationParams.getAlgorithm()); + writePropertyTag(xmlSerializer, TAG_SALT, keyDerivationParams.getSalt()); + writePropertyTag(xmlSerializer, TAG_MEMORY_DIFFICULTY, + keyDerivationParams.getMemoryDifficulty()); + } + + private static void writeKeyChainSnapshotProperties( + XmlSerializer xmlSerializer, KeyChainSnapshot keyChainSnapshot) + throws IOException, CertificateEncodingException { + + writePropertyTag(xmlSerializer, TAG_SNAPSHOT_VERSION, + keyChainSnapshot.getSnapshotVersion()); + writePropertyTag(xmlSerializer, TAG_MAX_ATTEMPTS, keyChainSnapshot.getMaxAttempts()); + writePropertyTag(xmlSerializer, TAG_COUNTER_ID, keyChainSnapshot.getCounterId()); + writePropertyTag(xmlSerializer, TAG_RECOVERY_KEY_MATERIAL, + keyChainSnapshot.getEncryptedRecoveryKeyBlob()); + writePropertyTag(xmlSerializer, TAG_SERVER_PARAMS, keyChainSnapshot.getServerParams()); + writePropertyTag(xmlSerializer, TAG_TRUSTED_HARDWARE_CERT_PATH, + keyChainSnapshot.getTrustedHardwareCertPath()); + } + + private static void writePropertyTag( + XmlSerializer xmlSerializer, String propertyName, long propertyValue) + throws IOException { + xmlSerializer.startTag(NAMESPACE, propertyName); + xmlSerializer.text(Long.toString(propertyValue)); + xmlSerializer.endTag(NAMESPACE, propertyName); + } + + private static void writePropertyTag( + XmlSerializer xmlSerializer, String propertyName, String propertyValue) + throws IOException { + xmlSerializer.startTag(NAMESPACE, propertyName); + xmlSerializer.text(propertyValue); + xmlSerializer.endTag(NAMESPACE, propertyName); + } + + private static void writePropertyTag( + XmlSerializer xmlSerializer, String propertyName, byte[] propertyValue) + throws IOException { + xmlSerializer.startTag(NAMESPACE, propertyName); + xmlSerializer.text(Base64.encodeToString(propertyValue, /*flags=*/ Base64.DEFAULT)); + xmlSerializer.endTag(NAMESPACE, propertyName); + } + + private static void writePropertyTag( + XmlSerializer xmlSerializer, String propertyName, CertPath certPath) + throws IOException, CertificateEncodingException { + writePropertyTag(xmlSerializer, propertyName, certPath.getEncoded(CERT_PATH_ENCODING)); + } + + // Statics only + private KeyChainSnapshotSerializer() {} +} diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializerTest.java new file mode 100644 index 0000000000000..6c2958e4f456c --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/serialization/KeyChainSnapshotSerializerTest.java @@ -0,0 +1,218 @@ +/* + * 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.google.common.truth.Truth.assertThat; + +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.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.server.locksettings.recoverablekeystore.TestData; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.security.cert.CertPath; +import java.util.ArrayList; +import java.util.List; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class KeyChainSnapshotSerializerTest { + private static final int COUNTER_ID = 2134; + private static final int SNAPSHOT_VERSION = 125; + private static final int MAX_ATTEMPTS = 21; + private static final byte[] SERVER_PARAMS = new byte[] { 8, 2, 4 }; + private static final byte[] KEY_BLOB = new byte[] { 124, 53, 53, 53 }; + private static final CertPath CERT_PATH = TestData.CERT_PATH_1; + private static final int SECRET_TYPE = KeyChainProtectionParams.TYPE_LOCKSCREEN; + private static final int LOCK_SCREEN_UI = KeyChainProtectionParams.UI_FORMAT_PASSWORD; + private static final byte[] SALT = new byte[] { 5, 4, 3, 2, 1 }; + private static final int MEMORY_DIFFICULTY = 45; + private static final int ALGORITHM = KeyDerivationParams.ALGORITHM_SCRYPT; + private static final byte[] SECRET = new byte[] { 1, 2, 3, 4 }; + + private static final String TEST_KEY_1_ALIAS = "key1"; + private static final byte[] TEST_KEY_1_BYTES = new byte[] { 66, 77, 88 }; + + private static final String TEST_KEY_2_ALIAS = "key2"; + private static final byte[] TEST_KEY_2_BYTES = new byte[] { 99, 33, 11 }; + + private static final String TEST_KEY_3_ALIAS = "key3"; + private static final byte[] TEST_KEY_3_BYTES = new byte[] { 2, 8, 100 }; + + @Test + public void roundTrip_persistsCounterId() throws Exception { + assertThat(roundTrip().getCounterId()).isEqualTo(COUNTER_ID); + } + + @Test + public void roundTrip_persistsSnapshotVersion() throws Exception { + assertThat(roundTrip().getSnapshotVersion()).isEqualTo(SNAPSHOT_VERSION); + } + + @Test + public void roundTrip_persistsMaxAttempts() throws Exception { + assertThat(roundTrip().getMaxAttempts()).isEqualTo(MAX_ATTEMPTS); + } + + @Test + public void roundTrip_persistsRecoveryKey() throws Exception { + assertThat(roundTrip().getEncryptedRecoveryKeyBlob()).isEqualTo(KEY_BLOB); + } + + @Test + public void roundTrip_persistsServerParams() throws Exception { + assertThat(roundTrip().getServerParams()).isEqualTo(SERVER_PARAMS); + } + + @Test + public void roundTrip_persistsCertPath() throws Exception { + assertThat(roundTrip().getTrustedHardwareCertPath()).isEqualTo(CERT_PATH); + } + + @Test + public void roundTrip_persistsParamsList() throws Exception { + assertThat(roundTrip().getKeyChainProtectionParams()).hasSize(1); + } + + @Test + public void roundTripParams_persistsUserSecretType() throws Exception { + assertThat(roundTripParams().getUserSecretType()).isEqualTo(SECRET_TYPE); + } + + @Test + public void roundTripParams_persistsLockScreenUi() throws Exception { + assertThat(roundTripParams().getLockScreenUiFormat()).isEqualTo(LOCK_SCREEN_UI); + } + + @Test + public void roundTripParams_persistsSalt() throws Exception { + assertThat(roundTripParams().getKeyDerivationParams().getSalt()).isEqualTo(SALT); + } + + @Test + public void roundTripParams_persistsAlgorithm() throws Exception { + assertThat(roundTripParams().getKeyDerivationParams().getAlgorithm()).isEqualTo(ALGORITHM); + } + + @Test + public void roundTripParams_persistsMemoryDifficulty() throws Exception { + assertThat(roundTripParams().getKeyDerivationParams().getMemoryDifficulty()) + .isEqualTo(MEMORY_DIFFICULTY); + } + + @Test + public void roundTripParams_doesNotPersistSecret() throws Exception { + assertThat(roundTripParams().getSecret()).isEmpty(); + } + + @Test + public void roundTripKeys_hasCorrectLength() throws Exception { + assertThat(roundTripKeys()).hasSize(3); + } + + @Test + public void roundTripKeys_0_persistsAlias() throws Exception { + assertThat(roundTripKeys().get(0).getAlias()).isEqualTo(TEST_KEY_1_ALIAS); + } + + @Test + public void roundTripKeys_0_persistsKeyBytes() throws Exception { + assertThat(roundTripKeys().get(0).getEncryptedKeyMaterial()).isEqualTo(TEST_KEY_1_BYTES); + } + + @Test + public void roundTripKeys_1_persistsAlias() throws Exception { + assertThat(roundTripKeys().get(1).getAlias()).isEqualTo(TEST_KEY_2_ALIAS); + } + + @Test + public void roundTripKeys_1_persistsKeyBytes() throws Exception { + assertThat(roundTripKeys().get(1).getEncryptedKeyMaterial()).isEqualTo(TEST_KEY_2_BYTES); + } + + @Test + public void roundTripKeys_2_persistsAlias() throws Exception { + assertThat(roundTripKeys().get(2).getAlias()).isEqualTo(TEST_KEY_3_ALIAS); + } + + @Test + public void roundTripKeys_2_persistsKeyBytes() throws Exception { + assertThat(roundTripKeys().get(2).getEncryptedKeyMaterial()).isEqualTo(TEST_KEY_3_BYTES); + } + + private static List roundTripKeys() throws Exception { + return roundTrip().getWrappedApplicationKeys(); + } + + private static KeyChainProtectionParams roundTripParams() throws Exception { + return roundTrip().getKeyChainProtectionParams().get(0); + } + + public static KeyChainSnapshot roundTrip() throws Exception { + KeyChainSnapshot snapshot = createTestKeyChainSnapshot(); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + KeyChainSnapshotSerializer.serialize(snapshot, byteArrayOutputStream); + return KeyChainSnapshotDeserializer.deserialize( + new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); + } + + private static KeyChainSnapshot createTestKeyChainSnapshot() throws Exception { + KeyDerivationParams keyDerivationParams = + KeyDerivationParams.createScryptParams(SALT, MEMORY_DIFFICULTY); + KeyChainProtectionParams keyChainProtectionParams = new KeyChainProtectionParams.Builder() + .setKeyDerivationParams(keyDerivationParams) + .setUserSecretType(SECRET_TYPE) + .setLockScreenUiFormat(LOCK_SCREEN_UI) + .setSecret(SECRET) + .build(); + ArrayList keyChainProtectionParamsList = + new ArrayList<>(1); + keyChainProtectionParamsList.add(keyChainProtectionParams); + + ArrayList keyList = new ArrayList<>(); + keyList.add(createKey(TEST_KEY_1_ALIAS, TEST_KEY_1_BYTES)); + keyList.add(createKey(TEST_KEY_2_ALIAS, TEST_KEY_2_BYTES)); + keyList.add(createKey(TEST_KEY_3_ALIAS, TEST_KEY_3_BYTES)); + + return new KeyChainSnapshot.Builder() + .setCounterId(COUNTER_ID) + .setSnapshotVersion(SNAPSHOT_VERSION) + .setServerParams(SERVER_PARAMS) + .setMaxAttempts(MAX_ATTEMPTS) + .setEncryptedRecoveryKeyBlob(KEY_BLOB) + .setKeyChainProtectionParams(keyChainProtectionParamsList) + .setWrappedApplicationKeys(keyList) + .setTrustedHardwareCertPath(CERT_PATH) + .build(); + } + + private static WrappedApplicationKey createKey(String alias, byte[] bytes) { + return new WrappedApplicationKey.Builder() + .setAlias(alias) + .setEncryptedKeyMaterial(bytes) + .build(); + } +}