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

Commit 8a782869 authored by Alex Klyubin's avatar Alex Klyubin Committed by Android Git Automerger
Browse files

am 7ca65f09: am b000d129: am 6a6f0c7d: Merge "Add HmacSHA256 backed by AndroidKeyStore."

* commit '7ca65f09':
  Add HmacSHA256 backed by AndroidKeyStore.
parents dc0078b7 7ca65f09
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -494,6 +494,19 @@ public class AndroidKeyStore extends KeyStoreSpi {
            args.addInt(KeymasterDefs.KM_TAG_DIGEST,
                    KeyStoreKeyConstraints.Digest.toKeymaster(digest));
        }
        if (keyAlgorithm == KeyStoreKeyConstraints.Algorithm.HMAC) {
            if (digest == null) {
                throw new IllegalStateException("Digest algorithm must be specified for key"
                        + " algorithm " + keyAlgorithmString);
            }
            Integer digestOutputSizeBytes =
                    KeyStoreKeyConstraints.Digest.getOutputSizeBytes(digest);
            if (digestOutputSizeBytes != null) {
                // TODO: Remove MAC length constraint once Keymaster API no longer requires it.
                // TODO: Switch to bits instead of bytes, once this is fixed in Keymaster
                args.addInt(KeymasterDefs.KM_TAG_MAC_LENGTH, digestOutputSizeBytes);
            }
        }

        @KeyStoreKeyConstraints.PurposeEnum int purposes = (params.getPurposes() != null)
                ? params.getPurposes()
+4 −0
Original line number Diff line number Diff line
@@ -39,5 +39,9 @@ public class AndroidKeyStoreProvider extends Provider {
        // javax.crypto.KeyGenerator
        put("KeyGenerator.AES", KeyStoreKeyGeneratorSpi.AES.class.getName());
        put("KeyGenerator.HmacSHA256", KeyStoreKeyGeneratorSpi.HmacSHA256.class.getName());

        // javax.crypto.Mac
        put("Mac.HmacSHA256", KeyStoreHmacSpi.HmacSHA256.class.getName());
        put("Mac.HmacSHA256 SupportedKeyClasses", KeyStoreSecretKey.class.getName());
    }
}
+12 −0
Original line number Diff line number Diff line
package android.security;

/**
 * Indicates a communications error with keystore service.
 *
 * @hide
 */
public class KeyStoreConnectException extends CryptoOperationException {
    public KeyStoreConnectException() {
        super("Failed to communicate with keystore service");
    }
}
+228 −0
Original line number Diff line number Diff line
package android.security;

import android.security.keymaster.OperationResult;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * Helper for streaming a crypto operation's input and output via {@link KeyStore} service's
 * {@code update} and {@code finish} operations.
 *
 * <p>The helper abstracts away to issues that need to be solved in most code that uses KeyStore's
 * update and finish operations. Firstly, KeyStore's update and finish operations can consume only a
 * limited amount of data in one go because the operations are marshalled via Binder. Secondly, the
 * update operation may consume less data than provided, in which case the caller has to buffer
 * the remainder for next time. The helper exposes {@link #update(byte[], int, int) update} and
 * {@link #doFinal(byte[], int, int) doFinal} operations which can be used to conveniently implement
 * various JCA crypto primitives.
 *
 * <p>KeyStore operation through which data is streamed is abstracted away as
 * {@link KeyStoreOperation} to avoid having this class deal with operation tokens and occasional
 * additional parameters to update and final operations.
 *
 * @hide
 */
public class KeyStoreCryptoOperationChunkedStreamer {
    public interface KeyStoreOperation {
        /**
         * Returns the result of the KeyStore update operation or null if keystore couldn't be
         * reached.
         */
        OperationResult update(byte[] input);

        /**
         * Returns the result of the KeyStore finish operation or null if keystore couldn't be
         * reached.
         */
        OperationResult finish(byte[] input);
    }

    // Binder buffer is about 1MB, but it's shared between all active transactions of the process.
    // Thus, it's safer to use a much smaller upper bound.
    private static final int DEFAULT_MAX_CHUNK_SIZE = 64 * 1024;
    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

    private final KeyStoreOperation mKeyStoreOperation;
    private final int mMaxChunkSize;

    private byte[] mBuffered = EMPTY_BYTE_ARRAY;
    private int mBufferedOffset;
    private int mBufferedLength;

    public KeyStoreCryptoOperationChunkedStreamer(KeyStoreOperation operation) {
        this(operation, DEFAULT_MAX_CHUNK_SIZE);
    }

    public KeyStoreCryptoOperationChunkedStreamer(KeyStoreOperation operation, int maxChunkSize) {
        mKeyStoreOperation = operation;
        mMaxChunkSize = maxChunkSize;
    }

    public byte[] update(byte[] input, int inputOffset, int inputLength) throws KeymasterException {
        if (inputLength == 0) {
            // No input provided
            return EMPTY_BYTE_ARRAY;
        }

        ByteArrayOutputStream bufferedOutput = null;

        while (inputLength > 0) {
            byte[] chunk;
            int inputBytesInChunk;
            if ((mBufferedLength + inputLength) > mMaxChunkSize) {
                // Too much input for one chunk -- extract one max-sized chunk and feed it into the
                // update operation.
                chunk = new byte[mMaxChunkSize];
                System.arraycopy(mBuffered, mBufferedOffset, chunk, 0, mBufferedLength);
                inputBytesInChunk = chunk.length - mBufferedLength;
                System.arraycopy(input, inputOffset, chunk, mBufferedLength, inputBytesInChunk);
            } else {
                // All of available input fits into one chunk.
                if ((mBufferedLength == 0) && (inputOffset == 0)
                        && (inputLength == input.length)) {
                    // Nothing buffered and all of input array needs to be fed into the update
                    // operation.
                    chunk = input;
                    inputBytesInChunk = input.length;
                } else {
                    // Need to combine buffered data with input data into one array.
                    chunk = new byte[mBufferedLength + inputLength];
                    inputBytesInChunk = inputLength;
                    System.arraycopy(mBuffered, mBufferedOffset, chunk, 0, mBufferedLength);
                    System.arraycopy(input, inputOffset, chunk, mBufferedLength, inputLength);
                }
            }
            // Update input array references to reflect that some of its bytes are now in mBuffered.
            inputOffset += inputBytesInChunk;
            inputLength -= inputBytesInChunk;

            OperationResult opResult = mKeyStoreOperation.update(chunk);
            if (opResult == null) {
                throw new KeyStoreConnectException();
            } else if (opResult.resultCode != KeyStore.NO_ERROR) {
                throw KeymasterUtils.getExceptionForKeymasterError(opResult.resultCode);
            }

            if (opResult.inputConsumed == chunk.length) {
                // The whole chunk was consumed
                mBuffered = EMPTY_BYTE_ARRAY;
                mBufferedOffset = 0;
                mBufferedLength = 0;
            } else if (opResult.inputConsumed == 0) {
                // Nothing was consumed. More input needed.
                if (inputLength > 0) {
                    // More input is available, but it wasn't included into the previous chunk
                    // because the chunk reached its maximum permitted size.
                    // Shouldn't have happened.
                    throw new CryptoOperationException("Nothing consumed from max-sized chunk: "
                            + chunk.length + " bytes");
                }
                mBuffered = chunk;
                mBufferedOffset = 0;
                mBufferedLength = chunk.length;
            } else if (opResult.inputConsumed < chunk.length) {
                // The chunk was consumed only partially -- buffer the rest of the chunk
                mBuffered = chunk;
                mBufferedOffset = opResult.inputConsumed;
                mBufferedLength = chunk.length - opResult.inputConsumed;
            } else {
                throw new CryptoOperationException("Consumed more than provided: "
                        + opResult.inputConsumed + ", provided: " + chunk.length);
            }

            if ((opResult.output != null) && (opResult.output.length > 0)) {
                if (inputLength > 0) {
                    // More output might be produced in this loop -- buffer the current output
                    if (bufferedOutput == null) {
                        bufferedOutput = new ByteArrayOutputStream();
                        try {
                            bufferedOutput.write(opResult.output);
                        } catch (IOException e) {
                            throw new CryptoOperationException("Failed to buffer output", e);
                        }
                    }
                } else {
                    // No more output will be produced in this loop
                    if (bufferedOutput == null) {
                        // No previously buffered output
                        return opResult.output;
                    } else {
                        // There was some previously buffered output
                        try {
                            bufferedOutput.write(opResult.output);
                        } catch (IOException e) {
                            throw new CryptoOperationException("Failed to buffer output", e);
                        }
                        return bufferedOutput.toByteArray();
                    }
                }
            }
        }

        if (bufferedOutput == null) {
            // No output produced
            return EMPTY_BYTE_ARRAY;
        } else {
            return bufferedOutput.toByteArray();
        }
    }

    public byte[] doFinal(byte[] input, int inputOffset, int inputLength)
            throws KeymasterException {
        if (inputLength == 0) {
            // No input provided -- simplify the rest of the code
            input = EMPTY_BYTE_ARRAY;
            inputOffset = 0;
        }

        byte[] updateOutput = null;
        if ((mBufferedLength + inputLength) > mMaxChunkSize) {
            updateOutput = update(input, inputOffset, inputLength);
            inputOffset += inputLength;
            inputLength = 0;
        }
        // All of available input fits into one chunk.

        byte[] finalChunk;
        if ((mBufferedLength == 0) && (inputOffset == 0)
                && (inputLength == input.length)) {
            // Nothing buffered and all of input array needs to be fed into the finish operation.
            finalChunk = input;
        } else {
            // Need to combine buffered data with input data into one array.
            finalChunk = new byte[mBufferedLength + inputLength];
            System.arraycopy(mBuffered, mBufferedOffset, finalChunk, 0, mBufferedLength);
            System.arraycopy(input, inputOffset, finalChunk, mBufferedLength, inputLength);
        }
        mBuffered = EMPTY_BYTE_ARRAY;
        mBufferedLength = 0;
        mBufferedOffset = 0;

        OperationResult opResult = mKeyStoreOperation.finish(finalChunk);
        if (opResult == null) {
            throw new KeyStoreConnectException();
        } else if (opResult.resultCode != KeyStore.NO_ERROR) {
            throw KeymasterUtils.getExceptionForKeymasterError(opResult.resultCode);
        }

        if (opResult.inputConsumed != finalChunk.length) {
            throw new CryptoOperationException("Unexpected number of bytes consumed by finish: "
                    + opResult.inputConsumed + " instead of " + finalChunk.length);
        }

        // Return the concatenation of the output of update and finish.
        byte[] result;
        byte[] finishOutput = opResult.output;
        if ((updateOutput == null) || (updateOutput.length == 0)) {
            result = finishOutput;
        } else if ((finishOutput == null) || (finishOutput.length == 0)) {
            result = updateOutput;
        } else {
            result = new byte[updateOutput.length + finishOutput.length];
            System.arraycopy(updateOutput, 0, result, 0, updateOutput.length);
            System.arraycopy(finishOutput, 0, result, updateOutput.length, finishOutput.length);
        }
        return (result != null) ? result : EMPTY_BYTE_ARRAY;
    }
}
+174 −0
Original line number Diff line number Diff line
package android.security;

import android.os.IBinder;
import android.security.keymaster.KeymasterArguments;
import android.security.keymaster.KeymasterDefs;
import android.security.keymaster.OperationResult;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.spec.AlgorithmParameterSpec;

import javax.crypto.MacSpi;

/**
 * {@link MacSpi} which provides HMAC implementations backed by Android KeyStore.
 *
 * @hide
 */
public abstract class KeyStoreHmacSpi extends MacSpi {

    public static class HmacSHA256 extends KeyStoreHmacSpi {
        public HmacSHA256() {
            super(KeyStoreKeyConstraints.Digest.SHA256, 256 / 8);
        }
    }

    private final KeyStore mKeyStore = KeyStore.getInstance();
    private final @KeyStoreKeyConstraints.DigestEnum int mDigest;
    private final int mMacSizeBytes;

    private String mKeyAliasInKeyStore;

    // The fields below are reset by the engineReset operation.
    private KeyStoreCryptoOperationChunkedStreamer mChunkedStreamer;
    private IBinder mOperationToken;

    protected KeyStoreHmacSpi(@KeyStoreKeyConstraints.DigestEnum int digest, int macSizeBytes) {
        mDigest = digest;
        mMacSizeBytes = macSizeBytes;
    }

    @Override
    protected int engineGetMacLength() {
        return mMacSizeBytes;
    }

    @Override
    protected void engineInit(Key key, AlgorithmParameterSpec params) throws InvalidKeyException,
            InvalidAlgorithmParameterException {
        if (key == null) {
            throw new InvalidKeyException("key == null");
        } else if (!(key instanceof KeyStoreSecretKey)) {
            throw new InvalidKeyException(
                    "Only Android KeyStore secret keys supported. Key: " + key);
        }

        if (params != null) {
            throw new InvalidAlgorithmParameterException(
                    "Unsupported algorithm parameters: " + params);
        }

        mKeyAliasInKeyStore = ((KeyStoreSecretKey) key).getAlias();
        engineReset();
    }

    @Override
    protected void engineReset() {
        IBinder operationToken = mOperationToken;
        if (operationToken != null) {
            mOperationToken = null;
            mKeyStore.abort(operationToken);
        }
        mChunkedStreamer = null;

        KeymasterArguments keymasterArgs = new KeymasterArguments();
        keymasterArgs.addInt(KeymasterDefs.KM_TAG_DIGEST, mDigest);

        OperationResult opResult = mKeyStore.begin(mKeyAliasInKeyStore,
                KeymasterDefs.KM_PURPOSE_SIGN,
                true,
                keymasterArgs,
                null,
                new KeymasterArguments());
        if (opResult == null) {
            throw new KeyStoreConnectException();
        } else if (opResult.resultCode != KeyStore.NO_ERROR) {
            throw new CryptoOperationException("Failed to start keystore operation",
                    KeymasterUtils.getExceptionForKeymasterError(opResult.resultCode));
        }
        mOperationToken = opResult.token;
        if (mOperationToken == null) {
            throw new CryptoOperationException("Keystore returned null operation token");
        }
        mChunkedStreamer = new KeyStoreCryptoOperationChunkedStreamer(
                new KeyStoreStreamingConsumer(mKeyStore, mOperationToken));
    }

    @Override
    protected void engineUpdate(byte input) {
        engineUpdate(new byte[] {input}, 0, 1);
    }

    @Override
    protected void engineUpdate(byte[] input, int offset, int len) {
        if (mChunkedStreamer == null) {
            throw new IllegalStateException("Not initialized");
        }

        byte[] output;
        try {
            output = mChunkedStreamer.update(input, offset, len);
        } catch (KeymasterException e) {
            throw new CryptoOperationException("Keystore operation failed", e);
        }
        if ((output != null) && (output.length != 0)) {
            throw new CryptoOperationException("Update operation unexpectedly produced output");
        }
    }

    @Override
    protected byte[] engineDoFinal() {
        if (mChunkedStreamer == null) {
            throw new IllegalStateException("Not initialized");
        }

        byte[] result;
        try {
            result = mChunkedStreamer.doFinal(null, 0, 0);
        } catch (KeymasterException e) {
            throw new CryptoOperationException("Keystore operation failed", e);
        }

        engineReset();
        return result;
    }

    @Override
    public void finalize() throws Throwable {
        try {
            IBinder operationToken = mOperationToken;
            if (operationToken != null) {
                mOperationToken = null;
                mKeyStore.abort(operationToken);
            }
        } finally {
            super.finalize();
        }
    }

    /**
     * KeyStore-backed consumer of {@code MacSpi}'s chunked stream.
     */
    private static class KeyStoreStreamingConsumer
            implements KeyStoreCryptoOperationChunkedStreamer.KeyStoreOperation {
        private final KeyStore mKeyStore;
        private final IBinder mOperationToken;

        private KeyStoreStreamingConsumer(KeyStore keyStore, IBinder operationToken) {
            mKeyStore = keyStore;
            mOperationToken = operationToken;
        }

        @Override
        public OperationResult update(byte[] input) {
            return mKeyStore.update(mOperationToken, null, input);
        }

        @Override
        public OperationResult finish(byte[] input) {
            return mKeyStore.finish(mOperationToken, null, input);
        }
    }
}
Loading