blob: dbcdf077ea16a08167ddff513858161b33d85e88 [file] [log] [blame]
/*
* Copyright 2021 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.nearby.common.bluetooth.fastpair;
import static com.google.common.primitives.Bytes.concat;
import androidx.annotation.Nullable;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.util.Arrays;
import javax.crypto.KeyAgreement;
/**
* Helper for generating keys based off of the Elliptic-Curve Diffie-Hellman algorithm (ECDH).
*/
public final class EllipticCurveDiffieHellmanExchange {
public static final int PUBLIC_KEY_LENGTH = 64;
static final int PRIVATE_KEY_LENGTH = 32;
private static final String[] PROVIDERS = {"GmsCore_OpenSSL", "AndroidOpenSSL", "SC", "BC"};
private static final String EC_ALGORITHM = "EC";
/**
* Also known as prime256v1 or NIST P-256.
*/
private static final ECGenParameterSpec EC_GEN_PARAMS = new ECGenParameterSpec("secp256r1");
@Nullable
private final ECPublicKey mPublicKey;
private final ECPrivateKey mPrivateKey;
/**
* Creates a new EllipticCurveDiffieHellmanExchange object.
*/
public static EllipticCurveDiffieHellmanExchange create() throws GeneralSecurityException {
KeyPair keyPair = generateKeyPair();
return new EllipticCurveDiffieHellmanExchange(
(ECPublicKey) keyPair.getPublic(), (ECPrivateKey) keyPair.getPrivate());
}
/**
* Creates a new EllipticCurveDiffieHellmanExchange object.
*/
public static EllipticCurveDiffieHellmanExchange create(byte[] privateKey)
throws GeneralSecurityException {
ECPrivateKey ecPrivateKey = (ECPrivateKey) generatePrivateKey(privateKey);
return new EllipticCurveDiffieHellmanExchange(/*publicKey=*/ null, ecPrivateKey);
}
private EllipticCurveDiffieHellmanExchange(
@Nullable ECPublicKey publicKey, ECPrivateKey privateKey) {
this.mPublicKey = publicKey;
this.mPrivateKey = privateKey;
}
/**
* @param otherPublicKey Another party's public key. See {@link #getPublicKey()} for format.
* @return The shared secret. Given our public key (and its private key), the other party can
* generate the same secret. This is a key meant for symmetric encryption.
*/
public byte[] generateSecret(byte[] otherPublicKey) throws GeneralSecurityException {
KeyAgreement agreement = keyAgreement();
agreement.init(mPrivateKey);
agreement.doPhase(generatePublicKey(otherPublicKey), /*lastPhase=*/ true);
byte[] secret = agreement.generateSecret();
// Headsets only support AES with 128-bit keys. So, hash the secret so that the entropy is
// high and then take only the first 128-bits.
secret = MessageDigest.getInstance("SHA-256").digest(secret);
return Arrays.copyOf(secret, 16);
}
/**
* Returns a public point W on the NIST P-256 elliptic curve. First 32 bytes are the X
* coordinate, next 32 bytes are the Y coordinate. Each coordinate is an unsigned big-endian
* integer.
*/
public @Nullable byte[] getPublicKey() {
if (mPublicKey == null) {
return null;
}
ECPoint w = mPublicKey.getW();
// See getPrivateKey for why we're resizing.
byte[] x = resizeWithLeadingZeros(w.getAffineX().toByteArray(), 32);
byte[] y = resizeWithLeadingZeros(w.getAffineY().toByteArray(), 32);
return concat(x, y);
}
/**
* Returns a private value S, an unsigned big-endian integer.
*/
public byte[] getPrivateKey() {
// Note that BigInteger.toByteArray() returns a signed representation, so it will add an
// extra zero byte to the front if the first bit is 1.
// We must remove that leading zero (we know the number is unsigned). We must also add
// leading zeros if the number is too small.
return resizeWithLeadingZeros(mPrivateKey.getS().toByteArray(), 32);
}
/**
* Removes or adds leading zeros until we have an array of size {@code n}.
*/
private static byte[] resizeWithLeadingZeros(byte[] x, int n) {
if (n < x.length) {
int start = x.length - n;
for (int i = 0; i < start; i++) {
if (x[i] != 0) {
throw new IllegalArgumentException(
"More than " + n + " non-zero bytes in " + Arrays.toString(x));
}
}
return Arrays.copyOfRange(x, start, x.length);
}
return concat(new byte[n - x.length], x);
}
/**
* @param publicKey See {@link #getPublicKey()} for format.
*/
private static PublicKey generatePublicKey(byte[] publicKey) throws GeneralSecurityException {
if (publicKey.length != PUBLIC_KEY_LENGTH) {
throw new GeneralSecurityException("Public key length incorrect: " + publicKey.length);
}
byte[] x = Arrays.copyOf(publicKey, publicKey.length / 2);
byte[] y = Arrays.copyOfRange(publicKey, publicKey.length / 2, publicKey.length);
return keyFactory()
.generatePublic(
new ECPublicKeySpec(
new ECPoint(new BigInteger(/*signum=*/ 1, x),
new BigInteger(/*signum=*/ 1, y)),
ecParameterSpec()));
}
/**
* @param privateKey See {@link #getPrivateKey()} for format.
*/
private static PrivateKey generatePrivateKey(byte[] privateKey)
throws GeneralSecurityException {
if (privateKey.length != PRIVATE_KEY_LENGTH) {
throw new GeneralSecurityException("Private key length incorrect: "
+ privateKey.length);
}
return keyFactory()
.generatePrivate(
new ECPrivateKeySpec(new BigInteger(/*signum=*/ 1, privateKey),
ecParameterSpec()));
}
private static ECParameterSpec ecParameterSpec() throws GeneralSecurityException {
// This seems to be the simplest way to get the curve's ECParameterSpec. Verified that it's
// the same whether you get it from the public or private key, and that it's the same as the
// raw params in SecAggEcUtil.getNistP256Params().
return ((ECPublicKey) generateKeyPair().getPublic()).getParams();
}
private static KeyPair generateKeyPair() throws GeneralSecurityException {
KeyPairGenerator generator = findProvider(p -> KeyPairGenerator.getInstance(EC_ALGORITHM,
p));
generator.initialize(EC_GEN_PARAMS);
return generator.generateKeyPair();
}
private static KeyAgreement keyAgreement() throws NoSuchProviderException {
return findProvider(p -> KeyAgreement.getInstance("ECDH", p));
}
private static KeyFactory keyFactory() throws NoSuchProviderException {
return findProvider(p -> KeyFactory.getInstance(EC_ALGORITHM, p));
}
private interface ProviderConsumer<T> {
T tryProvider(String provider) throws NoSuchAlgorithmException, NoSuchProviderException;
}
private static <T> T findProvider(ProviderConsumer<T> providerConsumer)
throws NoSuchProviderException {
for (String provider : PROVIDERS) {
try {
return providerConsumer.tryProvider(provider);
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
// No-op
}
}
throw new NoSuchProviderException();
}
}