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

Commit 1edc6a3c authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Implement startRecoverySession"

parents da8d32ef e16fa98a
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