Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e16fa98a authored by Robert Berry's avatar Robert Berry
Browse files

Implement startRecoverySession

Test: adb shell am instrument -w -e package com.android.server.locksettings.recoverablekeystore com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
Change-Id: I25e99f6014ef5e831420367040de7e1a80f134f0
parent 056b84b5
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -60,7 +60,7 @@ public final class KeyDerivationParameters implements Parcelable {
    /**
     * Creates instance of the class to to derive key using salted SHA256 hash.
     */
    public KeyDerivationParameters createSHA256Parameters(@NonNull byte[] salt) {
    public static KeyDerivationParameters createSHA256Parameters(@NonNull byte[] salt) {
        return new KeyDerivationParameters(ALGORITHM_SHA256, salt);
    }

+19 −0
Original line number Diff line number Diff line
@@ -20,10 +20,13 @@ import com.android.internal.annotations.VisibleForTesting;

import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;

@@ -39,6 +42,7 @@ import javax.crypto.SecretKey;
 */
public class KeySyncUtils {

    private static final String PUBLIC_KEY_FACTORY_ALGORITHM = "EC";
    private static final String RECOVERY_KEY_ALGORITHM = "AES";
    private static final int RECOVERY_KEY_SIZE_BITS = 256;

@@ -236,6 +240,21 @@ public class KeySyncUtils {
                /*encryptedPayload=*/ encryptedApplicationKey);
    }

    /**
     * Deserializes a X509 public key.
     *
     * @param key The bytes of the key.
     * @return The key.
     * @throws NoSuchAlgorithmException if the public key algorithm is unavailable.
     * @throws InvalidKeySpecException if the bytes of the key are not a valid key.
     */
    public static PublicKey deserializePublicKey(byte[] key)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeyFactory keyFactory = KeyFactory.getInstance(PUBLIC_KEY_FACTORY_ALGORITHM);
        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(key);
        return keyFactory.generatePublic(publicKeySpec);
    }

    /**
     * Returns the concatenation of all the given {@code arrays}.
     */
+62 −5
Original line number Diff line number Diff line
@@ -30,7 +30,13 @@ import android.security.recoverablekeystore.KeyStoreRecoveryMetadata;
import android.security.recoverablekeystore.RecoverableKeyStoreLoader;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.List;

@@ -44,7 +50,10 @@ public class RecoverableKeyStoreManager {
    private static final String TAG = "RecoverableKeyStoreManager";

    private static RecoverableKeyStoreManager mInstance;
    private Context mContext;

    private final Context mContext;
    private final RecoverableKeyStoreDb mDatabase;
    private final RecoverySessionStorage mRecoverySessionStorage;

    /**
     * Returns a new or existing instance.
@@ -53,14 +62,23 @@ public class RecoverableKeyStoreManager {
     */
    public static synchronized RecoverableKeyStoreManager getInstance(Context mContext) {
        if (mInstance == null) {
            mInstance = new RecoverableKeyStoreManager(mContext);
            RecoverableKeyStoreDb db = RecoverableKeyStoreDb.newInstance(mContext);
            mInstance = new RecoverableKeyStoreManager(
                    mContext.getApplicationContext(),
                    db,
                    new RecoverySessionStorage());
        }
        return mInstance;
    }

    @VisibleForTesting
    RecoverableKeyStoreManager(Context context) {
    RecoverableKeyStoreManager(
            Context context,
            RecoverableKeyStoreDb recoverableKeyStoreDb,
            RecoverySessionStorage recoverySessionStorage) {
        mContext = context;
        mDatabase = recoverableKeyStoreDb;
        mRecoverySessionStorage = recoverySessionStorage;
    }

    public int initRecoveryService(
@@ -161,7 +179,13 @@ public class RecoverableKeyStoreManager {
    /**
     * Initializes recovery session.
     *
     * @return recovery claim
     * @param sessionId A unique ID to identify the recovery session.
     * @param verifierPublicKey X509-encoded public key.
     * @param vaultParams Additional params associated with vault.
     * @param vaultChallenge Challenge issued by vault service.
     * @param secrets Lock-screen hashes. Should have a single element. TODO: why is this a list?
     * @return Encrypted bytes of recovery claim. This can then be issued to the vault service.
     *
     * @hide
     */
    public byte[] startRecoverySession(
@@ -173,7 +197,40 @@ public class RecoverableKeyStoreManager {
            int userId)
            throws RemoteException {
        checkRecoverKeyStorePermission();
        throw new UnsupportedOperationException();

        if (secrets.size() != 1) {
            // TODO: support multiple secrets
            throw new RemoteException("Only a single KeyStoreRecoveryMetadata is supported");
        }

        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
        byte[] kfHash = secrets.get(0).getSecret();
        mRecoverySessionStorage.add(
                userId, new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant));

        try {
            byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(kfHash);
            PublicKey publicKey = KeySyncUtils.deserializePublicKey(verifierPublicKey);
            return KeySyncUtils.encryptRecoveryClaim(
                    publicKey,
                    vaultParams,
                    vaultChallenge,
                    thmKfHash,
                    keyClaimant);
        } catch (NoSuchAlgorithmException e) {
            // Should never happen: all the algorithms used are required by AOSP implementations.
            throw new RemoteException(
                    "Missing required algorithm",
                    e,
                    /*enableSuppression=*/ true,
                    /*writeableStackTrace=*/ true);
        } catch (InvalidKeySpecException | InvalidKeyException e) {
            throw new RemoteException(
                    "Not a valid X509 key",
                    e,
                    /*enableSuppression=*/ true,
                    /*writeableStackTrace=*/ true);
        }
    }

    public void recoverKeys(
+173 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.storage;

import android.annotation.Nullable;
import android.util.SparseArray;

import java.util.ArrayList;
import java.util.Arrays;

import javax.security.auth.Destroyable;

/**
 * Stores pending recovery sessions in memory. We do not write these to disk, as it contains hashes
 * of the user's lock screen.
 *
 * @hide
 */
public class RecoverySessionStorage implements Destroyable {

    private final SparseArray<ArrayList<Entry>> mSessionsByUid = new SparseArray<>();

    /**
     * Returns the session for the given user with the given id.
     *
     * @param uid The uid of the recovery agent who created the session.
     * @param sessionId The unique identifier for the session.
     * @return The session info.
     *
     * @hide
     */
    @Nullable
    public Entry get(int uid, String sessionId) {
        ArrayList<Entry> userEntries = mSessionsByUid.get(uid);
        if (userEntries == null) {
            return null;
        }
        for (Entry entry : userEntries) {
            if (sessionId.equals(entry.mSessionId)) {
                return entry;
            }
        }
        return null;
    }

    /**
     * Adds a pending session for the given user.
     *
     * @param uid The uid of the recovery agent who created the session.
     * @param entry The session info.
     *
     * @hide
     */
    public void add(int uid, Entry entry) {
        if (mSessionsByUid.get(uid) == null) {
            mSessionsByUid.put(uid, new ArrayList<>());
        }
        mSessionsByUid.get(uid).add(entry);
    }

    /**
     * Removes all sessions associated with the given recovery agent uid.
     *
     * @param uid The uid of the recovery agent whose sessions to remove.
     *
     * @hide
     */
    public void remove(int uid) {
        ArrayList<Entry> entries = mSessionsByUid.get(uid);
        if (entries == null) {
            return;
        }
        for (Entry entry : entries) {
            entry.destroy();
        }
        mSessionsByUid.remove(uid);
    }

    /**
     * Returns the total count of pending sessions.
     *
     * @hide
     */
    public int size() {
        int size = 0;
        int numberOfUsers = mSessionsByUid.size();
        for (int i = 0; i < numberOfUsers; i++) {
            ArrayList<Entry> entries = mSessionsByUid.valueAt(i);
            size += entries.size();
        }
        return size;
    }

    /**
     * Wipes the memory of any sensitive information (i.e., lock screen hashes and key claimants).
     *
     * @hide
     */
    @Override
    public void destroy() {
        int numberOfUids = mSessionsByUid.size();
        for (int i = 0; i < numberOfUids; i++) {
            ArrayList<Entry> entries = mSessionsByUid.valueAt(i);
            for (Entry entry : entries) {
                entry.destroy();
            }
        }
    }

    /**
     * Information about a recovery session.
     *
     * @hide
     */
    public static class Entry implements Destroyable {
        private final byte[] mLskfHash;
        private final byte[] mKeyClaimant;
        private final String mSessionId;

        /**
         * @hide
         */
        public Entry(String sessionId, byte[] lskfHash, byte[] keyClaimant) {
            this.mLskfHash = lskfHash;
            this.mSessionId = sessionId;
            this.mKeyClaimant = keyClaimant;
        }

        /**
         * Returns the hash of the lock screen associated with the recovery attempt.
         *
         * @hide
         */
        public byte[] getLskfHash() {
            return mLskfHash;
        }

        /**
         * Returns the key generated for this recovery attempt (used to decrypt data returned by
         * the server).
         *
         * @hide
         */
        public byte[] getKeyClaimant() {
            return mKeyClaimant;
        }

        /**
         * Overwrites the memory for the lskf hash and key claimant.
         *
         * @hide
         */
        @Override
        public void destroy() {
            Arrays.fill(mLskfHash, (byte) 0);
            Arrays.fill(mKeyClaimant, (byte) 0);
        }
    }
}
+186 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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;

import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_LOCKSCREEN;
import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_PASSWORD;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import android.content.Context;
import android.os.RemoteException;
import android.security.recoverablekeystore.KeyDerivationParameters;
import android.security.recoverablekeystore.KeyStoreRecoveryMetadata;
import android.security.recoverablekeystore.RecoverableKeyStoreLoader;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;

import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;

import com.google.common.collect.ImmutableList;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.io.File;
import java.nio.charset.StandardCharsets;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class RecoverableKeyStoreManagerTest {
    private static final String DATABASE_FILE_NAME = "recoverablekeystore.db";

    private static final String TEST_SESSION_ID = "karlin";
    private static final byte[] TEST_PUBLIC_KEY = new byte[] {
        (byte) 0x30, (byte) 0x59, (byte) 0x30, (byte) 0x13, (byte) 0x06, (byte) 0x07, (byte) 0x2a,
        (byte) 0x86, (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x02, (byte) 0x01, (byte) 0x06,
        (byte) 0x08, (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0xce, (byte) 0x3d, (byte) 0x03,
        (byte) 0x01, (byte) 0x07, (byte) 0x03, (byte) 0x42, (byte) 0x00, (byte) 0x04, (byte) 0xb8,
        (byte) 0x00, (byte) 0x11, (byte) 0x18, (byte) 0x98, (byte) 0x1d, (byte) 0xf0, (byte) 0x6e,
        (byte) 0xb4, (byte) 0x94, (byte) 0xfe, (byte) 0x86, (byte) 0xda, (byte) 0x1c, (byte) 0x07,
        (byte) 0x8d, (byte) 0x01, (byte) 0xb4, (byte) 0x3a, (byte) 0xf6, (byte) 0x8d, (byte) 0xdc,
        (byte) 0x61, (byte) 0xd0, (byte) 0x46, (byte) 0x49, (byte) 0x95, (byte) 0x0f, (byte) 0x10,
        (byte) 0x86, (byte) 0x93, (byte) 0x24, (byte) 0x66, (byte) 0xe0, (byte) 0x3f, (byte) 0xd2,
        (byte) 0xdf, (byte) 0xf3, (byte) 0x79, (byte) 0x20, (byte) 0x1d, (byte) 0x91, (byte) 0x55,
        (byte) 0xb0, (byte) 0xe5, (byte) 0xbd, (byte) 0x7a, (byte) 0x8b, (byte) 0x32, (byte) 0x7d,
        (byte) 0x25, (byte) 0x53, (byte) 0xa2, (byte) 0xfc, (byte) 0xa5, (byte) 0x65, (byte) 0xe1,
        (byte) 0xbd, (byte) 0x21, (byte) 0x44, (byte) 0x7e, (byte) 0x78, (byte) 0x52, (byte) 0xfa};
    private static final byte[] TEST_SALT = getUtf8Bytes("salt");
    private static final byte[] TEST_SECRET = getUtf8Bytes("password1234");
    private static final byte[] TEST_VAULT_CHALLENGE = getUtf8Bytes("vault_challenge");
    private static final byte[] TEST_VAULT_PARAMS = getUtf8Bytes("vault_params");
    private static final int TEST_USER_ID = 10009;
    private static final int KEY_CLAIMANT_LENGTH_BYTES = 16;

    @Mock private Context mMockContext;

    private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
    private File mDatabaseFile;
    private RecoverableKeyStoreManager mRecoverableKeyStoreManager;
    private RecoverySessionStorage mRecoverySessionStorage;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        Context context = InstrumentationRegistry.getTargetContext();
        mDatabaseFile = context.getDatabasePath(DATABASE_FILE_NAME);
        mRecoverableKeyStoreDb = RecoverableKeyStoreDb.newInstance(context);
        mRecoverySessionStorage = new RecoverySessionStorage();
        mRecoverableKeyStoreManager = new RecoverableKeyStoreManager(
                mMockContext,
                mRecoverableKeyStoreDb,
                mRecoverySessionStorage);
    }

    @After
    public void tearDown() {
        mRecoverableKeyStoreDb.close();
        mDatabaseFile.delete();
    }

    @Test
    public void startRecoverySession_checksPermissionFirst() throws Exception {
        mRecoverableKeyStoreManager.startRecoverySession(
                TEST_SESSION_ID,
                TEST_PUBLIC_KEY,
                TEST_VAULT_PARAMS,
                TEST_VAULT_CHALLENGE,
                ImmutableList.of(new KeyStoreRecoveryMetadata(
                        TYPE_LOCKSCREEN,
                        TYPE_PASSWORD,
                        KeyDerivationParameters.createSHA256Parameters(TEST_SALT),
                        TEST_SECRET)),
                TEST_USER_ID);

        verify(mMockContext, times(1)).enforceCallingOrSelfPermission(
                eq(RecoverableKeyStoreLoader.PERMISSION_RECOVER_KEYSTORE),
                any());
    }

    @Test
    public void startRecoverySession_storesTheSessionInfo() throws Exception {
        mRecoverableKeyStoreManager.startRecoverySession(
                TEST_SESSION_ID,
                TEST_PUBLIC_KEY,
                TEST_VAULT_PARAMS,
                TEST_VAULT_CHALLENGE,
                ImmutableList.of(new KeyStoreRecoveryMetadata(
                        TYPE_LOCKSCREEN,
                        TYPE_PASSWORD,
                        KeyDerivationParameters.createSHA256Parameters(TEST_SALT),
                        TEST_SECRET)),
                TEST_USER_ID);

        assertEquals(1, mRecoverySessionStorage.size());
        RecoverySessionStorage.Entry entry = mRecoverySessionStorage.get(
                TEST_USER_ID, TEST_SESSION_ID);
        assertArrayEquals(TEST_SECRET, entry.getLskfHash());
        assertEquals(KEY_CLAIMANT_LENGTH_BYTES, entry.getKeyClaimant().length);
    }

    @Test
    public void startRecoverySession_throwsIfBadNumberOfSecrets() throws Exception {
        try {
            mRecoverableKeyStoreManager.startRecoverySession(
                    TEST_SESSION_ID,
                    TEST_PUBLIC_KEY,
                    TEST_VAULT_PARAMS,
                    TEST_VAULT_CHALLENGE,
                    ImmutableList.of(),
                    TEST_USER_ID);
        } catch (RemoteException e) {
            assertEquals("Only a single KeyStoreRecoveryMetadata is supported",
                    e.getMessage());
        }
    }

    @Test
    public void startRecoverySession_throwsIfBadKey() throws Exception {
        try {
            mRecoverableKeyStoreManager.startRecoverySession(
                    TEST_SESSION_ID,
                    getUtf8Bytes("0"),
                    TEST_VAULT_PARAMS,
                    TEST_VAULT_CHALLENGE,
                    ImmutableList.of(new KeyStoreRecoveryMetadata(
                            TYPE_LOCKSCREEN,
                            TYPE_PASSWORD,
                            KeyDerivationParameters.createSHA256Parameters(TEST_SALT),
                            TEST_SECRET)),
                    TEST_USER_ID);
        } catch (RemoteException e) {
            assertEquals("Not a valid X509 key",
                    e.getMessage());
        }
    }

    private static byte[] getUtf8Bytes(String s) {
        return s.getBytes(StandardCharsets.UTF_8);
    }
}
Loading