blob: 794c19d31874edb3cec592a0aeca7b0d23da2f29 [file] [log] [blame]
/*
* Copyright (C) 2022 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 android.nearby.fastpair.provider.crypto;
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import com.google.protobuf.ByteString;
import java.math.BigInteger;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.EllipticCurve;
import java.util.Collections;
/** Provides methods for calculating E2EE EIDs and E2E encryption/decryption based on E2EE EIDs. */
public final class E2eeCalculator {
private static final byte[] TEMP_KEY_PADDING_1 =
Bytes.toArray(Collections.nCopies(11, (byte) 0xFF));
private static final byte[] TEMP_KEY_PADDING_2 = new byte[11];
private static final ECParameterSpec CURVE_SPEC = getCurveSpec();
private static final BigInteger P = ((ECFieldFp) CURVE_SPEC.getCurve().getField()).getP();
private static final BigInteger TWO = new BigInteger("2");
private static final BigInteger THREE = new BigInteger("3");
private static final int E2EE_EID_IDENTITY_KEY_SIZE = 32;
private static final int E2EE_EID_SIZE = 20;
/**
* Computes the E2EE EID value for the given device clock based time. Note that Eddystone
* beacons start advertising the new EID at a random time within the window, therefore the
* currently advertised EID for beacon time <em>t</em> may be either
* {@code computeE2eeEid(eik, k, t)} or {@code computeE2eeEid(eik, k, t - (1 << k))}.
*
* <p>The E2EE EID computation is based on https://goto.google.com/e2ee-eid-computation.
*
* @param identityKey the beacon's 32-byte Eddystone E2EE identity key
* @param exponent rotation period exponent as configured on the beacon, must be in
* range the [0,15]
* @param deviceClockSeconds the value of the beacon's 32-bit seconds time counter (treated as
* an unsigned value)
* @return E2EE EID value.
*/
public static ByteString computeE2eeEid(
ByteString identityKey, int exponent, int deviceClockSeconds) {
return computePublicKey(computePrivateKey(identityKey, exponent, deviceClockSeconds));
}
private static ByteString computePublicKey(BigInteger privateKey) {
return getXCoordinateBytes(toPoint(privateKey));
}
private static BigInteger computePrivateKey(
ByteString identityKey, int exponent, int deviceClockSeconds) {
Preconditions.checkArgument(
Preconditions.checkNotNull(identityKey).size() == E2EE_EID_IDENTITY_KEY_SIZE);
Preconditions.checkArgument(exponent >= 0 && exponent < 16);
byte[] exponentByte = new byte[]{(byte) exponent};
byte[] paddedCounter = Ints.toByteArray((deviceClockSeconds >>> exponent) << exponent);
byte[] data =
Bytes.concat(
TEMP_KEY_PADDING_1,
exponentByte,
paddedCounter,
TEMP_KEY_PADDING_2,
exponentByte,
paddedCounter);
byte[] rTag =
Crypto.aesEcbNoPaddingEncrypt(identityKey, ByteString.copyFrom(data)).toByteArray();
return new BigInteger(1, rTag).mod(CURVE_SPEC.getOrder());
}
private static ECPoint toPoint(BigInteger privateKey) {
return multiplyPoint(CURVE_SPEC.getGenerator(), privateKey);
}
private static ByteString getXCoordinateBytes(ECPoint point) {
byte[] unalignedBytes = point.getAffineX().toByteArray();
// The unalignedBytes may have length < 32 if the leading E2EE EID bytes are zero, or
// it may be E2EE_EID_SIZE + 1 if the leading bit is 1, in which case the first byte is
// always zero.
Verify.verify(
unalignedBytes.length <= E2EE_EID_SIZE
|| (unalignedBytes.length == E2EE_EID_SIZE + 1 && unalignedBytes[0] == 0));
byte[] bytes;
if (unalignedBytes.length < E2EE_EID_SIZE) {
bytes = new byte[E2EE_EID_SIZE];
System.arraycopy(
unalignedBytes, 0, bytes, bytes.length - unalignedBytes.length,
unalignedBytes.length);
} else if (unalignedBytes.length == E2EE_EID_SIZE + 1) {
bytes = new byte[E2EE_EID_SIZE];
System.arraycopy(unalignedBytes, 1, bytes, 0, E2EE_EID_SIZE);
} else { // unalignedBytes.length == GattE2EE_EID_SIZE
bytes = unalignedBytes;
}
return ByteString.copyFrom(bytes);
}
/** Returns a secp160r1 curve spec. */
private static ECParameterSpec getCurveSpec() {
final BigInteger p = new BigInteger("ffffffffffffffffffffffffffffffff7fffffff", 16);
final BigInteger n = new BigInteger("0100000000000000000001f4c8f927aed3ca752257", 16);
final BigInteger a = new BigInteger("ffffffffffffffffffffffffffffffff7ffffffc", 16);
final BigInteger b = new BigInteger("1c97befc54bd7a8b65acf89f81d4d4adc565fa45", 16);
final BigInteger gx = new BigInteger("4a96b5688ef573284664698968c38bb913cbfc82", 16);
final BigInteger gy = new BigInteger("23a628553168947d59dcc912042351377ac5fb32", 16);
final int h = 1;
ECFieldFp fp = new ECFieldFp(p);
EllipticCurve spec = new EllipticCurve(fp, a, b);
ECPoint g = new ECPoint(gx, gy);
return new ECParameterSpec(spec, g, n, h);
}
/** Returns the scalar multiplication result of k*p in Fp. */
private static ECPoint multiplyPoint(ECPoint p, BigInteger k) {
ECPoint r = ECPoint.POINT_INFINITY;
ECPoint s = p;
BigInteger kModP = k.mod(P);
int length = kModP.bitLength();
for (int i = 0; i <= length - 1; i++) {
if (kModP.mod(TWO).byteValue() == 1) {
r = addPoint(r, s);
}
s = doublePoint(s);
kModP = kModP.divide(TWO);
}
return r;
}
/** Returns the point addition r+s in Fp. */
private static ECPoint addPoint(ECPoint r, ECPoint s) {
if (r.equals(s)) {
return doublePoint(r);
} else if (r.equals(ECPoint.POINT_INFINITY)) {
return s;
} else if (s.equals(ECPoint.POINT_INFINITY)) {
return r;
}
BigInteger slope =
r.getAffineY()
.subtract(s.getAffineY())
.multiply(r.getAffineX().subtract(s.getAffineX()).modInverse(P))
.mod(P);
BigInteger x =
slope.modPow(TWO, P).subtract(r.getAffineX()).subtract(s.getAffineX()).mod(P);
BigInteger y = s.getAffineY().negate().mod(P);
y = y.add(slope.multiply(s.getAffineX().subtract(x))).mod(P);
return new ECPoint(x, y);
}
/** Returns the point doubling 2*r in Fp. */
private static ECPoint doublePoint(ECPoint r) {
if (r.equals(ECPoint.POINT_INFINITY)) {
return r;
}
BigInteger slope = r.getAffineX().pow(2).multiply(THREE);
slope = slope.add(CURVE_SPEC.getCurve().getA());
slope = slope.multiply(r.getAffineY().multiply(TWO).modInverse(P));
BigInteger x = slope.pow(2).subtract(r.getAffineX().multiply(TWO)).mod(P);
BigInteger y =
r.getAffineY().negate().add(slope.multiply(r.getAffineX().subtract(x))).mod(P);
return new ECPoint(x, y);
}
private E2eeCalculator() {
}
}