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

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

Merge "Implement recoverKeys"

parents 5f51741a b9a220b9
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
@@ -54,6 +54,8 @@ public class KeySyncUtils {
            "V1 encrypted_application_key".getBytes(StandardCharsets.UTF_8);
    private static final byte[] RECOVERY_CLAIM_HEADER =
            "V1 KF_claim".getBytes(StandardCharsets.UTF_8);
    private static final byte[] RECOVERY_RESPONSE_HEADER =
            "V1 reencrypted_recovery_key".getBytes(StandardCharsets.UTF_8);

    private static final byte[] THM_KF_HASH_PREFIX = "THM_KF_hash".getBytes(StandardCharsets.UTF_8);

@@ -203,6 +205,28 @@ public class KeySyncUtils {
                /*payload=*/ concat(thmKfHash, keyClaimant));
    }

    /**
     * Decrypts response from recovery claim, returning the locally encrypted key.
     *
     * @param keyClaimant The key claimant, used by the remote service to encrypt the response.
     * @param vaultParams Vault params associated with the claim.
     * @param encryptedResponse The encrypted response.
     * @return The locally encrypted recovery key.
     * @throws NoSuchAlgorithmException if any SecureBox algorithm is not present.
     * @throws InvalidKeyException if the {@code keyClaimant} could not be used to decrypt.
     * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
     *     different key.
     */
    public static byte[] decryptRecoveryClaimResponse(
            byte[] keyClaimant, byte[] vaultParams, byte[] encryptedResponse)
            throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
        return SecureBox.decrypt(
                /*ourPrivateKey=*/ null,
                /*sharedSecret=*/ keyClaimant,
                /*header=*/ concat(RECOVERY_RESPONSE_HEADER, vaultParams),
                /*encryptedPayload=*/ encryptedResponse);
    }

    /**
     * Decrypts a recovery key, after having retrieved it from a remote server.
     *
+97 −5
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;

import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -42,10 +43,13 @@ import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.crypto.AEADBadTagException;

/**
 * Class with {@link RecoverableKeyStoreLoader} API implementation and internal methods to interact
 * with {@code LockSettingsService}.
@@ -225,7 +229,7 @@ public class RecoverableKeyStoreManager {
     * @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?
     * @param secrets Lock-screen hashes. For now only a single secret is supported.
     * @return Encrypted bytes of recovery claim. This can then be issued to the vault service.
     *
     * @hide
@@ -248,7 +252,8 @@ public class RecoverableKeyStoreManager {
        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
        byte[] kfHash = secrets.get(0).getSecret();
        mRecoverySessionStorage.add(
                userId, new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant));
                userId,
                new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant, vaultParams));

        try {
            byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(kfHash);
@@ -275,14 +280,101 @@ public class RecoverableKeyStoreManager {
        }
    }

    /**
     * Invoked by a recovery agent after a successful recovery claim is sent to the remote vault
     * service.
     *
     * <p>TODO: should also load into AndroidKeyStore.
     *
     * @param sessionId The session ID used to generate the claim. See
     *     {@link #startRecoverySession(String, byte[], byte[], byte[], List, int)}.
     * @param encryptedRecoveryKey The encrypted recovery key blob returned by the remote vault
     *     service.
     * @param applicationKeys The encrypted key blobs returned by the remote vault service. These
     *     were wrapped with the recovery key.
     * @param uid The uid of the recovery agent.
     * @throws RemoteException if an error occurred recovering the keys.
     */
    public void recoverKeys(
            @NonNull String sessionId,
            @NonNull byte[] recoveryKeyBlob,
            @NonNull byte[] encryptedRecoveryKey,
            @NonNull List<KeyEntryRecoveryData> applicationKeys,
            int userId)
            int uid)
            throws RemoteException {
        checkRecoverKeyStorePermission();
        throw new UnsupportedOperationException();

        RecoverySessionStorage.Entry sessionEntry = mRecoverySessionStorage.get(uid, sessionId);
        if (sessionEntry == null) {
            throw new RemoteException(String.format(Locale.US,
                    "User %d does not have pending session '%s'", uid, sessionId));
        }

        try {
            byte[] recoveryKey = decryptRecoveryKey(sessionEntry, encryptedRecoveryKey);
            recoverApplicationKeys(recoveryKey, applicationKeys);
        } finally {
            sessionEntry.destroy();
            mRecoverySessionStorage.remove(uid);
        }
    }

    private byte[] decryptRecoveryKey(
            RecoverySessionStorage.Entry sessionEntry, byte[] encryptedClaimResponse)
            throws RemoteException {
        try {
            byte[] locallyEncryptedKey = KeySyncUtils.decryptRecoveryClaimResponse(
                    sessionEntry.getKeyClaimant(),
                    sessionEntry.getVaultParams(),
                    encryptedClaimResponse);
            return KeySyncUtils.decryptRecoveryKey(sessionEntry.getLskfHash(), locallyEncryptedKey);
        } catch (InvalidKeyException | AEADBadTagException e) {
            throw new RemoteException(
                    "Failed to decrypt recovery key",
                    e,
                    /*enableSuppression=*/ true,
                    /*writeableStackTrace=*/ true);
        } 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);
        }
    }

    /**
     * Uses {@code recoveryKey} to decrypt {@code applicationKeys}.
     *
     * <p>TODO: and load them into store?
     *
     * @throws RemoteException if an error occurred decrypting the keys.
     */
    private void recoverApplicationKeys(
            @NonNull byte[] recoveryKey,
            @NonNull List<KeyEntryRecoveryData> applicationKeys) throws RemoteException {
        for (KeyEntryRecoveryData applicationKey : applicationKeys) {
            String alias = new String(applicationKey.getAlias(), StandardCharsets.UTF_8);
            byte[] encryptedKeyMaterial = applicationKey.getEncryptedKeyMaterial();

            try {
                // TODO: put decrypted key material in appropriate AndroidKeyStore
                KeySyncUtils.decryptApplicationKey(recoveryKey, encryptedKeyMaterial);
            } 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 (InvalidKeyException | AEADBadTagException e) {
                throw new RemoteException(
                        "Failed to recover key with alias '" + alias + "'",
                        e,
                    /*enableSuppression=*/ true,
                    /*writeableStackTrace=*/ true);
            }
        }
    }

    /**
+8 −5
Original line number Diff line number Diff line
@@ -230,7 +230,7 @@ public class SecureBox {
     * @throws NoSuchAlgorithmException if any underlying crypto algorithm is not supported
     * @throws InvalidKeyException if the provided key is invalid for underlying crypto algorithms
     * @throws AEADBadTagException if the authentication tag contained in {@code encryptedPayload}
     *     cannot be validated
     *     cannot be validated, or if the payload is not a valid SecureBox V2 payload.
     * @hide
     */
    public static byte[] decrypt(
@@ -244,12 +244,14 @@ public class SecureBox {
            throw new IllegalArgumentException("Both the private key and shared secret are empty");
        }
        header = emptyByteArrayIfNull(header);
        encryptedPayload = emptyByteArrayIfNull(encryptedPayload);
        if (encryptedPayload == null) {
            throw new NullPointerException("Encrypted payload must not be null.");
        }

        ByteBuffer ciphertextBuffer = ByteBuffer.wrap(encryptedPayload);
        byte[] version = readEncryptedPayload(ciphertextBuffer, VERSION.length);
        if (!Arrays.equals(version, VERSION)) {
            throw new IllegalArgumentException("The payload was not encrypted by SecureBox v2");
            throw new AEADBadTagException("The payload was not encrypted by SecureBox v2");
        }

        byte[] senderPublicKeyBytes;
@@ -271,12 +273,13 @@ public class SecureBox {
        return aesGcmDecrypt(decryptionKey, randNonce, ciphertext, header);
    }

    private static byte[] readEncryptedPayload(ByteBuffer buffer, int length) {
    private static byte[] readEncryptedPayload(ByteBuffer buffer, int length)
            throws AEADBadTagException {
        byte[] output = new byte[length];
        try {
            buffer.get(output);
        } catch (BufferUnderflowException ex) {
            throw new IllegalArgumentException("The encrypted payload is too short");
            throw new AEADBadTagException("The encrypted payload is too short");
        }
        return output;
    }
+15 −4
Original line number Diff line number Diff line
@@ -129,15 +129,17 @@ public class RecoverySessionStorage implements Destroyable {
    public static class Entry implements Destroyable {
        private final byte[] mLskfHash;
        private final byte[] mKeyClaimant;
        private final byte[] mVaultParams;
        private final String mSessionId;

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

        /**
@@ -159,6 +161,15 @@ public class RecoverySessionStorage implements Destroyable {
            return mKeyClaimant;
        }

        /**
         * Returns the vault params associated with the session.
         *
         * @hide
         */
        public byte[] getVaultParams() {
            return mVaultParams;
        }

        /**
         * Overwrites the memory for the lskf hash and key claimant.
         *
+38 −0
Original line number Diff line number Diff line
@@ -55,6 +55,8 @@ public class KeySyncUtilsTest {
            utf8Bytes("snQzsbvclkSsG6PwasAp1oFLzbq3KtFe");
    private static final byte[] RECOVERY_CLAIM_HEADER =
            "V1 KF_claim".getBytes(StandardCharsets.UTF_8);
    private static final byte[] RECOVERY_RESPONSE_HEADER =
            "V1 reencrypted_recovery_key".getBytes(StandardCharsets.UTF_8);

    @Test
    public void calculateThmKfHash_isShaOfLockScreenHashWithPrefix() throws Exception {
@@ -172,6 +174,42 @@ public class KeySyncUtilsTest {
        }
    }

    @Test
    public void decryptRecoveryClaimResponse_decryptsAValidResponse() throws Exception {
        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
        byte[] vaultParams = randomBytes(100);
        byte[] recoveryKey = randomBytes(32);
        byte[] encryptedPayload = SecureBox.encrypt(
                /*theirPublicKey=*/ null,
                /*sharedSecret=*/ keyClaimant,
                /*header=*/ KeySyncUtils.concat(RECOVERY_RESPONSE_HEADER, vaultParams),
                /*payload=*/ recoveryKey);

        byte[] decrypted = KeySyncUtils.decryptRecoveryClaimResponse(
                keyClaimant, vaultParams, encryptedPayload);

        assertArrayEquals(recoveryKey, decrypted);
    }

    @Test
    public void decryptRecoveryClaimResponse_throwsIfCannotDecrypt() throws Exception {
        byte[] vaultParams = randomBytes(100);
        byte[] recoveryKey = randomBytes(32);
        byte[] encryptedPayload = SecureBox.encrypt(
                /*theirPublicKey=*/ null,
                /*sharedSecret=*/ KeySyncUtils.generateKeyClaimant(),
                /*header=*/ KeySyncUtils.concat(RECOVERY_RESPONSE_HEADER, vaultParams),
                /*payload=*/ recoveryKey);

        try {
            KeySyncUtils.decryptRecoveryClaimResponse(
                    KeySyncUtils.generateKeyClaimant(), vaultParams, encryptedPayload);
            fail("Did not throw decrypting with bad keyClaimant");
        } catch (AEADBadTagException error) {
            // expected
        }
    }

    @Test
    public void encryptRecoveryClaim_encryptsLockScreenAndKeyClaimant() throws Exception {
        KeyPair keyPair = SecureBox.genKeyPair();
Loading